roll to 5a279edc239f4efd91223ddc609c2b5129325feb
diff --git a/engine/lib/src/platform_dispatcher.dart b/engine/lib/src/platform_dispatcher.dart
index 305c704..d7837b0 100644
--- a/engine/lib/src/platform_dispatcher.dart
+++ b/engine/lib/src/platform_dispatcher.dart
@@ -122,11 +122,14 @@
   /// the application.
   ///
   /// If any of their configurations change, [onMetricsChanged] will be called.
-  Iterable<FlutterView> get views => _views.values;
-  Map<Object, FlutterView> _views = <Object, FlutterView>{};
+  Iterable<FlutterView> get views => viewData.values;
+  Map<Object, FlutterView> viewData = <Object, FlutterView>{};
 
-  // A map of opaque platform view identifiers to view configurations.
-  Map<Object, ViewConfiguration> _viewConfigurations = <Object, ViewConfiguration>{};
+  Map<Object, ViewConfiguration> get windowConfigurations => _windowConfigurations;
+  final Map<Object, ViewConfiguration> _windowConfigurations =
+      <Object, ViewConfiguration>{};
+
+  FlutterView? get implicitView => viewData[kImplicitViewId];
 
   /// A callback that is invoked whenever the [ViewConfiguration] of any of the
   /// [views] changes.
@@ -181,33 +184,33 @@
     List<int> displayFeaturesState,
   ) {
     final ViewConfiguration previousConfiguration =
-        _viewConfigurations[id] ?? const ViewConfiguration();
-    if (!_views.containsKey(id)) {
-      _views[id] = FlutterWindow._(id, this);
+        _windowConfigurations[id] ?? const ViewConfiguration();
+    if (!viewData.containsKey(id)) {
+      viewData[id] = FlutterView._(id, this);
     }
-    _viewConfigurations[id] = previousConfiguration.copyWith(
-      window: _views[id],
+    _windowConfigurations[id] = previousConfiguration.copyWith(
+      window: viewData[id],
       devicePixelRatio: devicePixelRatio,
       geometry: Rect.fromLTWH(0.0, 0.0, width, height),
-      viewPadding: WindowPadding._(
+      viewPadding: ViewPadding._(
         top: viewPaddingTop,
         right: viewPaddingRight,
         bottom: viewPaddingBottom,
         left: viewPaddingLeft,
       ),
-      viewInsets: WindowPadding._(
+      viewInsets: ViewPadding._(
         top: viewInsetTop,
         right: viewInsetRight,
         bottom: viewInsetBottom,
         left: viewInsetLeft,
       ),
-      padding: WindowPadding._(
+      padding: ViewPadding._(
         top: math.max(0.0, viewPaddingTop - viewInsetTop),
         right: math.max(0.0, viewPaddingRight - viewInsetRight),
         bottom: math.max(0.0, viewPaddingBottom - viewInsetBottom),
         left: math.max(0.0, viewPaddingLeft - viewInsetLeft),
       ),
-      systemGestureInsets: WindowPadding._(
+      systemGestureInsets: ViewPadding._(
         top: math.max(0.0, systemGestureInsetTop),
         right: math.max(0.0, systemGestureInsetRight),
         bottom: math.max(0.0, systemGestureInsetBottom),
@@ -1019,10 +1022,10 @@
     this.devicePixelRatio = 1.0,
     this.geometry = Rect.zero,
     this.visible = false,
-    this.viewInsets = WindowPadding.zero,
-    this.viewPadding = WindowPadding.zero,
-    this.systemGestureInsets = WindowPadding.zero,
-    this.padding = WindowPadding.zero,
+    this.viewInsets = ViewPadding.zero,
+    this.viewPadding = ViewPadding.zero,
+    this.systemGestureInsets = ViewPadding.zero,
+    this.padding = ViewPadding.zero,
     this.gestureSettings = const GestureSettings(),
     this.displayFeatures = const <DisplayFeature>[],
   });
@@ -1032,10 +1035,10 @@
     double? devicePixelRatio,
     Rect? geometry,
     bool? visible,
-    WindowPadding? viewInsets,
-    WindowPadding? viewPadding,
-    WindowPadding? systemGestureInsets,
-    WindowPadding? padding,
+    ViewPadding? viewInsets,
+    ViewPadding? viewPadding,
+    ViewPadding? systemGestureInsets,
+    ViewPadding? padding,
     GestureSettings? gestureSettings,
     List<DisplayFeature>? displayFeatures,
   }) {
@@ -1057,10 +1060,10 @@
   final double devicePixelRatio;
   final Rect geometry;
   final bool visible;
-  final WindowPadding viewInsets;
-  final WindowPadding viewPadding;
-  final WindowPadding systemGestureInsets;
-  final WindowPadding padding;
+  final ViewPadding viewInsets;
+  final ViewPadding viewPadding;
+  final ViewPadding systemGestureInsets;
+  final ViewPadding padding;
   final GestureSettings gestureSettings;
   final List<DisplayFeature> displayFeatures;
 
@@ -1250,43 +1253,25 @@
   detached,
 }
 
-/// A representation of distances for each of the four edges of a rectangle,
-/// used to encode the view insets and padding that applications should place
-/// around their user interface, as exposed by [FlutterView.viewInsets] and
-/// [FlutterView.padding]. View insets and padding are preferably read via
-/// [MediaQuery.of].
-///
-/// For a generic class that represents distances around a rectangle, see the
-/// [EdgeInsets] class.
-///
-/// See also:
-///
-///  * [WidgetsBindingObserver], for a widgets layer mechanism to receive
-///    notifications when the padding changes.
-///  * [MediaQuery.of], for the preferred mechanism for accessing these values.
-///  * [Scaffold], which automatically applies the padding in material design
-///    applications.
-class WindowPadding {
-  const WindowPadding._({ required this.left, required this.top, required this.right, required this.bottom });
+@Deprecated(
+  'Use ViewPadding instead. '
+  'This feature was deprecated after v3.8.0-14.0.pre.',
+)
+typedef WindowPadding = ViewPadding;
 
-  /// The distance from the left edge to the first unpadded pixel, in physical pixels.
+class ViewPadding {
+  const ViewPadding._({ required this.left, required this.top, required this.right, required this.bottom });
+
   final double left;
-
-  /// The distance from the top edge to the first unpadded pixel, in physical pixels.
   final double top;
-
-  /// The distance from the right edge to the first unpadded pixel, in physical pixels.
   final double right;
-
-  /// The distance from the bottom edge to the first unpadded pixel, in physical pixels.
   final double bottom;
 
-  /// A window padding that has zeros for each edge.
-  static const WindowPadding zero = WindowPadding._(left: 0.0, top: 0.0, right: 0.0, bottom: 0.0);
+  static const ViewPadding zero = ViewPadding._(left: 0.0, top: 0.0, right: 0.0, bottom: 0.0);
 
   @override
   String toString() {
-    return 'WindowPadding(left: $left, top: $top, right: $right, bottom: $bottom)';
+    return 'ViewPadding(left: $left, top: $top, right: $right, bottom: $bottom)';
   }
 }
 
diff --git a/engine/lib/src/window.dart b/engine/lib/src/window.dart
index db46749..378d534 100644
--- a/engine/lib/src/window.dart
+++ b/engine/lib/src/window.dart
@@ -7,254 +7,35 @@
 
 part of dart.ui;
 
-/// A view into which a Flutter [Scene] is drawn.
-///
-/// Each [FlutterView] has its own layer tree that is rendered into an area
-/// inside of a [FlutterWindow] whenever [render] is called with a [Scene].
-///
-/// ## Insets and Padding
-///
-/// {@animation 300 300 https://flutter.github.io/assets-for-api-docs/assets/widgets/window_padding.mp4}
-///
-/// In this illustration, the black areas represent system UI that the app
-/// cannot draw over. The red area represents view padding that the view may not
-/// be able to detect gestures in and may not want to draw in. The grey area
-/// represents the system keyboard, which can cover over the bottom view padding
-/// when visible.
-///
-/// The [viewInsets] are the physical pixels which the operating
-/// system reserves for system UI, such as the keyboard, which would fully
-/// obscure any content drawn in that area.
-///
-/// The [viewPadding] are the physical pixels on each side of the
-/// display that may be partially obscured by system UI or by physical
-/// intrusions into the display, such as an overscan region on a television or a
-/// "notch" on a phone. Unlike the insets, these areas may have portions that
-/// show the user view-painted pixels without being obscured, such as a
-/// notch at the top of a phone that covers only a subset of the area. Insets,
-/// on the other hand, either partially or fully obscure the window, such as an
-/// opaque keyboard or a partially translucent status bar, which cover an area
-/// without gaps.
-///
-/// The [padding] property is computed from both
-/// [viewInsets] and [viewPadding]. It will allow a
-/// view inset to consume view padding where appropriate, such as when a phone's
-/// keyboard is covering the bottom view padding and so "absorbs" it.
-///
-/// Clients that want to position elements relative to the view padding
-/// regardless of the view insets should use the [viewPadding]
-/// property, e.g. if you wish to draw a widget at the center of the screen with
-/// respect to the iPhone "safe area" regardless of whether the keyboard is
-/// showing.
-///
-/// [padding] is useful for clients that want to know how much
-/// padding should be accounted for without concern for the current inset(s)
-/// state, e.g. determining whether a gesture should be considered for scrolling
-/// purposes. This value varies based on the current state of the insets. For
-/// example, a visible keyboard will consume all gestures in the bottom part of
-/// the [viewPadding] anyway, so there is no need to account for
-/// that in the [padding], which is always safe to use for such
-/// calculations.
-///
-/// See also:
-///
-///  * [FlutterWindow], a special case of a [FlutterView] that is represented on
-///    the platform as a separate window which can host other [FlutterView]s.
-abstract class FlutterView {
+const int kImplicitViewId = 0;
+
+class FlutterView {
+  FlutterView._(this.viewId, this.platformDispatcher);
+
+  /// The opaque ID for this view.
+  final Object viewId;
+
   /// The platform dispatcher that this view is registered with, and gets its
   /// information from.
-  PlatformDispatcher get platformDispatcher;
+  final PlatformDispatcher platformDispatcher;
 
-  /// The configuration of this view.
-  ViewConfiguration get viewConfiguration;
+  ViewConfiguration get _viewConfiguration {
+    final PlatformDispatcher engineDispatcher = platformDispatcher;
+    assert(engineDispatcher.windowConfigurations.containsKey(viewId));
+    return engineDispatcher.windowConfigurations[viewId] ??
+        const ViewConfiguration();
+  }
 
-  /// The number of device pixels for each logical pixel for the screen this
-  /// view is displayed on.
-  ///
-  /// This number might not be a power of two. Indeed, it might not even be an
-  /// integer. For example, the Nexus 6 has a device pixel ratio of 3.5.
-  ///
-  /// Device pixels are also referred to as physical pixels. Logical pixels are
-  /// also referred to as device-independent or resolution-independent pixels.
-  ///
-  /// By definition, there are roughly 38 logical pixels per centimeter, or
-  /// about 96 logical pixels per inch, of the physical display. The value
-  /// returned by [devicePixelRatio] is ultimately obtained either from the
-  /// hardware itself, the device drivers, or a hard-coded value stored in the
-  /// operating system or firmware, and may be inaccurate, sometimes by a
-  /// significant margin.
-  ///
-  /// The Flutter framework operates in logical pixels, so it is rarely
-  /// necessary to directly deal with this property.
-  ///
-  /// When this changes, [onMetricsChanged] is called.
-  ///
-  /// See also:
-  ///
-  ///  * [WidgetsBindingObserver], for a mechanism at the widgets layer to
-  ///    observe when this value changes.
-  double get devicePixelRatio => viewConfiguration.devicePixelRatio;
+  double get devicePixelRatio => _viewConfiguration.devicePixelRatio;
+  Rect get physicalGeometry => _viewConfiguration.geometry;
+  Size get physicalSize => _viewConfiguration.geometry.size;
+  ViewPadding get viewInsets => _viewConfiguration.viewInsets;
+  ViewPadding get viewPadding => _viewConfiguration.viewPadding;
+  ViewPadding get systemGestureInsets => _viewConfiguration.systemGestureInsets;
+  ViewPadding get padding => _viewConfiguration.padding;
+  GestureSettings get gestureSettings => _viewConfiguration.gestureSettings;
+  List<DisplayFeature> get displayFeatures => _viewConfiguration.displayFeatures;
 
-  /// The dimensions and location of the rectangle into which the scene rendered
-  /// in this view will be drawn on the screen, in physical pixels.
-  ///
-  /// When this changes, [onMetricsChanged] is called.
-  ///
-  /// At startup, the size and location of the view may not be known before Dart
-  /// code runs. If this value is observed early in the application lifecycle,
-  /// it may report [Rect.zero].
-  ///
-  /// This value does not take into account any on-screen keyboards or other
-  /// system UI. The [padding] and [viewInsets] properties provide a view into
-  /// how much of each side of the view may be obscured by system UI.
-  ///
-  /// See also:
-  ///
-  ///  * [WidgetsBindingObserver], for a mechanism at the widgets layer to
-  ///    observe when this value changes.
-  Rect get physicalGeometry => viewConfiguration.geometry;
-
-  /// The dimensions of the rectangle into which the scene rendered in this view
-  /// will be drawn on the screen, in physical pixels.
-  ///
-  /// When this changes, [onMetricsChanged] is called.
-  ///
-  /// At startup, the size of the view may not be known before Dart code runs.
-  /// If this value is observed early in the application lifecycle, it may
-  /// report [Size.zero].
-  ///
-  /// This value does not take into account any on-screen keyboards or other
-  /// system UI. The [padding] and [viewInsets] properties provide information
-  /// about how much of each side of the view may be obscured by system UI.
-  ///
-  /// This value is the same as the `size` member of [physicalGeometry].
-  ///
-  /// See also:
-  ///
-  ///  * [physicalGeometry], which reports the location of the view as well as
-  ///    its size.
-  ///  * [WidgetsBindingObserver], for a mechanism at the widgets layer to
-  ///    observe when this value changes.
-  Size get physicalSize => viewConfiguration.geometry.size;
-
-  /// The number of physical pixels on each side of the display rectangle into
-  /// which the view can render, but over which the operating system will likely
-  /// place system UI, such as the keyboard, that fully obscures any content.
-  ///
-  /// When this property changes, [onMetricsChanged] is called.
-  ///
-  /// The relationship between this [viewInsets],
-  /// [viewPadding], and [padding] are described in
-  /// more detail in the documentation for [FlutterView].
-  ///
-  /// See also:
-  ///
-  ///  * [WidgetsBindingObserver], for a mechanism at the widgets layer to
-  ///    observe when this value changes.
-  ///  * [MediaQuery.of], a simpler mechanism for the same.
-  ///  * [Scaffold], which automatically applies the view insets in material
-  ///    design applications.
-  WindowPadding get viewInsets => viewConfiguration.viewInsets;
-
-  /// The number of physical pixels on each side of the display rectangle into
-  /// which the view can render, but which may be partially obscured by system
-  /// UI (such as the system notification area), or or physical intrusions in
-  /// the display (e.g. overscan regions on television screens or phone sensor
-  /// housings).
-  ///
-  /// Unlike [padding], this value does not change relative to
-  /// [viewInsets]. For example, on an iPhone X, it will not
-  /// change in response to the soft keyboard being visible or hidden, whereas
-  /// [padding] will.
-  ///
-  /// When this property changes, [onMetricsChanged] is called.
-  ///
-  /// The relationship between this [viewInsets],
-  /// [viewPadding], and [padding] are described in
-  /// more detail in the documentation for [FlutterView].
-  ///
-  /// See also:
-  ///
-  ///  * [WidgetsBindingObserver], for a mechanism at the widgets layer to
-  ///    observe when this value changes.
-  ///  * [MediaQuery.of], a simpler mechanism for the same.
-  ///  * [Scaffold], which automatically applies the padding in material design
-  ///    applications.
-  WindowPadding get viewPadding => viewConfiguration.viewPadding;
-
-  /// The number of physical pixels on each side of the display rectangle into
-  /// which the view can render, but where the operating system will consume
-  /// input gestures for the sake of system navigation.
-  ///
-  /// For example, an operating system might use the vertical edges of the
-  /// screen, where swiping inwards from the edges takes users backward
-  /// through the history of screens they previously visited.
-  ///
-  /// When this property changes, [onMetricsChanged] is called.
-  ///
-  /// See also:
-  ///
-  ///  * [WidgetsBindingObserver], for a mechanism at the widgets layer to
-  ///    observe when this value changes.
-  ///  * [MediaQuery.of], a simpler mechanism for the same.
-  WindowPadding get systemGestureInsets => viewConfiguration.systemGestureInsets;
-
-  /// The number of physical pixels on each side of the display rectangle into
-  /// which the view can render, but which may be partially obscured by system
-  /// UI (such as the system notification area), or or physical intrusions in
-  /// the display (e.g. overscan regions on television screens or phone sensor
-  /// housings).
-  ///
-  /// This value is calculated by taking `max(0.0, FlutterView.viewPadding -
-  /// FlutterView.viewInsets)`. This will treat a system IME that increases the
-  /// bottom inset as consuming that much of the bottom padding. For example, on
-  /// an iPhone X, [EdgeInsets.bottom] of [FlutterView.padding] is the same as
-  /// [EdgeInsets.bottom] of [FlutterView.viewPadding] when the soft keyboard is
-  /// not drawn (to account for the bottom soft button area), but will be `0.0`
-  /// when the soft keyboard is visible.
-  ///
-  /// When this changes, [onMetricsChanged] is called.
-  ///
-  /// The relationship between this [viewInsets], [viewPadding], and [padding]
-  /// are described in more detail in the documentation for [FlutterView].
-  ///
-  /// See also:
-  ///
-  /// * [WidgetsBindingObserver], for a mechanism at the widgets layer to
-  ///   observe when this value changes.
-  /// * [MediaQuery.of], a simpler mechanism for the same.
-  /// * [Scaffold], which automatically applies the padding in material design
-  ///   applications.
-  WindowPadding get padding => viewConfiguration.padding;
-
-  /// Updates the view's rendering on the GPU with the newly provided [Scene].
-  ///
-  /// This function must be called within the scope of the
-  /// [PlatformDispatcher.onBeginFrame] or [PlatformDispatcher.onDrawFrame]
-  /// callbacks being invoked.
-  ///
-  /// If this function is called a second time during a single
-  /// [PlatformDispatcher.onBeginFrame]/[PlatformDispatcher.onDrawFrame]
-  /// callback sequence or called outside the scope of those callbacks, the call
-  /// will be ignored.
-  ///
-  /// To record graphical operations, first create a [PictureRecorder], then
-  /// construct a [Canvas], passing that [PictureRecorder] to its constructor.
-  /// After issuing all the graphical operations, call the
-  /// [PictureRecorder.endRecording] function on the [PictureRecorder] to obtain
-  /// the final [Picture] that represents the issued graphical operations.
-  ///
-  /// Next, create a [SceneBuilder], and add the [Picture] to it using
-  /// [SceneBuilder.addPicture]. With the [SceneBuilder.build] method you can
-  /// then obtain a [Scene] object, which you can display to the user via this
-  /// [render] function.
-  ///
-  /// See also:
-  ///
-  /// * [SchedulerBinding], the Flutter framework class which manages the
-  ///   scheduling of frames.
-  /// * [RendererBinding], the Flutter framework class which manages layout and
-  ///   painting.
   void render(Scene scene) {
     // TODO(yjbanov): implement a basic preroll for better benchmark realism.
   }
@@ -262,58 +43,9 @@
   void updateSemantics(SemanticsUpdate update) {
     platformDispatcher.updateSemantics(update);
   }
-
-  List<DisplayFeature> get displayFeatures => viewConfiguration.displayFeatures;
 }
 
-/// A top-level platform window displaying a Flutter layer tree drawn from a
-/// [Scene].
-///
-/// The current list of all Flutter views for the application is available from
-/// `WidgetsBinding.instance.platformDispatcher.views`. Only views that are of type
-/// [FlutterWindow] are top level platform windows.
-///
-/// There is also a [PlatformDispatcher.instance] singleton object in `dart:ui`
-/// if `WidgetsBinding` is unavailable, but we strongly advise avoiding a static
-/// reference to it. See the documentation for [PlatformDispatcher.instance] for
-/// more details about why it should be avoided.
-///
-/// See also:
-///
-/// * [PlatformDispatcher], which manages the current list of [FlutterView] (and
-///   thus [FlutterWindow]) instances.
-class FlutterWindow extends FlutterView {
-  FlutterWindow._(this._windowId, this.platformDispatcher);
-
-  /// The opaque ID for this view.
-  final Object _windowId;
-
-  @override
-  final PlatformDispatcher platformDispatcher;
-
-  @override
-  ViewConfiguration get viewConfiguration {
-    assert(platformDispatcher._viewConfigurations.containsKey(_windowId));
-    return platformDispatcher._viewConfigurations[_windowId]!;
-  }
-}
-
-/// A [FlutterWindow] that includes access to setting callbacks and retrieving
-/// properties that reside on the [PlatformDispatcher].
-///
-/// It is the type of the global [window] singleton used by applications that
-/// only have a single main window.
-///
-/// In addition to the properties of [FlutterView], this class provides access
-/// to platform-specific properties. To modify or retrieve these properties,
-/// applications designed for more than one main window should prefer using
-/// `WidgetsBinding.instance.platformDispatcher` instead.
-///
-/// Prefer access through `WidgetsBinding.instance.window` or
-/// `WidgetsBinding.instance.platformDispatcher` over a static reference to
-/// [window], or [PlatformDispatcher.instance]. See the documentation for
-/// [PlatformDispatcher.instance] for more details about this recommendation.
-class SingletonFlutterWindow extends FlutterWindow {
+class SingletonFlutterWindow extends FlutterView {
   SingletonFlutterWindow._(Object windowId, PlatformDispatcher platformDispatcher)
       : super._(windowId, platformDispatcher) {
     platformDispatcher._updateLifecycleState('resumed');
@@ -343,412 +75,99 @@
     );
   }
 
-  /// A callback that is invoked whenever the [devicePixelRatio],
-  /// [physicalSize], [padding], [viewInsets], [PlatformDispatcher.views], or
-  /// [systemGestureInsets] values change.
-  ///
-  /// {@macro dart.ui.window.accessorForwardWarning}
-  ///
-  /// See [PlatformDispatcher.onMetricsChanged] for more information.
   VoidCallback? get onMetricsChanged => platformDispatcher.onMetricsChanged;
   set onMetricsChanged(VoidCallback? callback) {
     platformDispatcher.onMetricsChanged = callback;
   }
 
-  /// The system-reported default locale of the device.
-  ///
-  /// {@template dart.ui.window.accessorForwardWarning}
-  /// Accessing this value returns the value contained in the
-  /// [PlatformDispatcher] singleton, so instead of getting it from here, you
-  /// should consider getting it from `WidgetsBinding.instance.platformDispatcher` instead
-  /// (or, when `WidgetsBinding` isn't available, from
-  /// [PlatformDispatcher.instance]). The reason this value forwards to the
-  /// [PlatformDispatcher] is to provide convenience for applications that only
-  /// use a single main window.
-  /// {@endtemplate}
-  ///
-  /// This establishes the language and formatting conventions that window
-  /// should, if possible, use to render their user interface.
-  ///
-  /// This is the first locale selected by the user and is the user's primary
-  /// locale (the locale the device UI is displayed in)
-  ///
-  /// This is equivalent to `locales.first` and will provide an empty non-null
-  /// locale if the [locales] list has not been set or is empty.
   Locale get locale => platformDispatcher.locale;
 
-  /// The full system-reported supported locales of the device.
-  ///
-  /// {@macro dart.ui.window.accessorForwardWarning}
-  ///
-  /// This establishes the language and formatting conventions that window
-  /// should, if possible, use to render their user interface.
-  ///
-  /// The list is ordered in order of priority, with lower-indexed locales being
-  /// preferred over higher-indexed ones. The first element is the primary [locale].
-  ///
-  /// The [onLocaleChanged] callback is called whenever this value changes.
-  ///
-  /// See also:
-  ///
-  ///  * [WidgetsBindingObserver], for a mechanism at the widgets layer to
-  ///    observe when this value changes.
   List<Locale> get locales => platformDispatcher.locales;
 
-  /// Performs the platform-native locale resolution.
-  ///
-  /// Each platform may return different results.
-  ///
-  /// If the platform fails to resolve a locale, then this will return null.
-  ///
-  /// This method returns synchronously and is a direct call to
-  /// platform specific APIs without invoking method channels.
   Locale? computePlatformResolvedLocale(List<Locale> supportedLocales) {
     return platformDispatcher.computePlatformResolvedLocale(supportedLocales);
   }
 
-  /// A callback that is invoked whenever [locale] changes value.
-  ///
-  /// {@macro dart.ui.window.accessorForwardWarning}
-  ///
-  /// The framework invokes this callback in the same zone in which the
-  /// callback was set.
-  ///
-  /// See also:
-  ///
-  ///  * [WidgetsBindingObserver], for a mechanism at the widgets layer to
-  ///    observe when this callback is invoked.
   VoidCallback? get onLocaleChanged => platformDispatcher.onLocaleChanged;
   set onLocaleChanged(VoidCallback? callback) {
     platformDispatcher.onLocaleChanged = callback;
   }
 
-  /// The lifecycle state immediately after dart isolate initialization.
-  ///
-  /// {@macro dart.ui.window.accessorForwardWarning}
-  ///
-  /// This property will not be updated as the lifecycle changes.
-  ///
-  /// It is used to initialize [SchedulerBinding.lifecycleState] at startup
-  /// with any buffered lifecycle state events.
   String get initialLifecycleState => platformDispatcher.initialLifecycleState;
 
-  /// The system-reported text scale.
-  ///
-  /// {@macro dart.ui.window.accessorForwardWarning}
-  ///
-  /// This establishes the text scaling factor to use when rendering text,
-  /// according to the user's platform preferences.
-  ///
-  /// The [onTextScaleFactorChanged] callback is called whenever this value
-  /// changes.
-  ///
-  /// See also:
-  ///
-  ///  * [WidgetsBindingObserver], for a mechanism at the widgets layer to
-  ///    observe when this value changes.
   double get textScaleFactor => platformDispatcher.textScaleFactor;
 
-  /// The setting indicating whether time should always be shown in the 24-hour
-  /// format.
-  ///
-  /// {@macro dart.ui.window.accessorForwardWarning}
-  ///
-  /// This option is used by [showTimePicker].
   bool get alwaysUse24HourFormat => platformDispatcher.alwaysUse24HourFormat;
 
-  /// A callback that is invoked whenever [textScaleFactor] changes value.
-  ///
-  /// {@macro dart.ui.window.accessorForwardWarning}
-  ///
-  /// The framework invokes this callback in the same zone in which the
-  /// callback was set.
-  ///
-  /// See also:
-  ///
-  ///  * [WidgetsBindingObserver], for a mechanism at the widgets layer to
-  ///    observe when this callback is invoked.
   VoidCallback? get onTextScaleFactorChanged => platformDispatcher.onTextScaleFactorChanged;
   set onTextScaleFactorChanged(VoidCallback? callback) {
     platformDispatcher.onTextScaleFactorChanged = callback;
   }
 
-  /// The setting indicating the current brightness mode of the host platform.
-  ///
-  /// {@macro dart.ui.window.accessorForwardWarning}
-  ///
-  /// If the platform has no preference, [platformBrightness] defaults to
-  /// [Brightness.light].
   Brightness get platformBrightness => platformDispatcher.platformBrightness;
 
-  /// A callback that is invoked whenever [platformBrightness] changes value.
-  ///
-  /// {@macro dart.ui.window.accessorForwardWarning}
-  ///
-  /// The framework invokes this callback in the same zone in which the
-  /// callback was set.
-  ///
-  /// See also:
-  ///
-  ///  * [WidgetsBindingObserver], for a mechanism at the widgets layer to
-  ///    observe when this callback is invoked.
   VoidCallback? get onPlatformBrightnessChanged => platformDispatcher.onPlatformBrightnessChanged;
   set onPlatformBrightnessChanged(VoidCallback? callback) {
     platformDispatcher.onPlatformBrightnessChanged = callback;
   }
 
-  /// A callback that is invoked to notify the window that it is an appropriate
-  /// time to provide a scene using the [SceneBuilder] API and the [render]
-  /// method.
-  ///
-  /// {@macro dart.ui.window.accessorForwardWarning}
-  ///
-  /// When possible, this is driven by the hardware VSync signal. This is only
-  /// called if [scheduleFrame] has been called since the last time this
-  /// callback was invoked.
-  ///
-  /// The [onDrawFrame] callback is invoked immediately after [onBeginFrame],
-  /// after draining any microtasks (e.g. completions of any [Future]s) queued
-  /// by the [onBeginFrame] handler.
-  ///
-  /// The framework invokes this callback in the same zone in which the
-  /// callback was set.
-  ///
-  /// See also:
-  ///
-  ///  * [SchedulerBinding], the Flutter framework class which manages the
-  ///    scheduling of frames.
-  ///  * [RendererBinding], the Flutter framework class which manages layout and
-  ///    painting.
   FrameCallback? get onBeginFrame => platformDispatcher.onBeginFrame;
   set onBeginFrame(FrameCallback? callback) {
     platformDispatcher.onBeginFrame = callback;
   }
 
-  /// A callback that is invoked for each frame after [onBeginFrame] has
-  /// completed and after the microtask queue has been drained.
-  ///
-  /// {@macro dart.ui.window.accessorForwardWarning}
-  ///
-  /// This can be used to implement a second phase of frame rendering that
-  /// happens after any deferred work queued by the [onBeginFrame] phase.
-  ///
-  /// The framework invokes this callback in the same zone in which the
-  /// callback was set.
-  ///
-  /// See also:
-  ///
-  ///  * [SchedulerBinding], the Flutter framework class which manages the
-  ///    scheduling of frames.
-  ///  * [RendererBinding], the Flutter framework class which manages layout and
-  ///    painting.
   VoidCallback? get onDrawFrame => platformDispatcher.onDrawFrame;
   set onDrawFrame(VoidCallback? callback) {
     platformDispatcher.onDrawFrame = callback;
   }
 
-  /// A callback that is invoked to report the [FrameTiming] of recently
-  /// rasterized frames.
-  ///
-  /// {@macro dart.ui.window.accessorForwardWarning}
-  ///
-  /// It's prefered to use [SchedulerBinding.addTimingsCallback] than to use
-  /// [SingletonFlutterWindow.onReportTimings] directly because
-  /// [SchedulerBinding.addTimingsCallback] allows multiple callbacks.
-  ///
-  /// This can be used to see if the window has missed frames (through
-  /// [FrameTiming.buildDuration] and [FrameTiming.rasterDuration]), or high
-  /// latencies (through [FrameTiming.totalSpan]).
-  ///
-  /// Unlike [Timeline], the timing information here is available in the release
-  /// mode (additional to the profile and the debug mode). Hence this can be
-  /// used to monitor the application's performance in the wild.
-  ///
-  /// {@macro dart.ui.TimingsCallback.list}
-  ///
-  /// If this is null, no additional work will be done. If this is not null,
-  /// Flutter spends less than 0.1ms every 1 second to report the timings
-  /// (measured on iPhone6S). The 0.1ms is about 0.6% of 16ms (frame budget for
-  /// 60fps), or 0.01% CPU usage per second.
   TimingsCallback? get onReportTimings => platformDispatcher.onReportTimings;
   set onReportTimings(TimingsCallback? callback) {
     platformDispatcher.onReportTimings = callback;
   }
 
-  /// A callback that is invoked when pointer data is available.
-  ///
-  /// {@macro dart.ui.window.accessorForwardWarning}
-  ///
-  /// The framework invokes this callback in the same zone in which the
-  /// callback was set.
-  ///
-  /// See also:
-  ///
-  ///  * [GestureBinding], the Flutter framework class which manages pointer
-  ///    events.
   PointerDataPacketCallback? get onPointerDataPacket => platformDispatcher.onPointerDataPacket;
   set onPointerDataPacket(PointerDataPacketCallback? callback) {
     platformDispatcher.onPointerDataPacket = callback;
   }
 
-  /// The route or path that the embedder requested when the application was
-  /// launched.
-  ///
-  /// {@macro dart.ui.window.accessorForwardWarning}
-  ///
-  /// This will be the string "`/`" if no particular route was requested.
-  ///
-  /// ## Android
-  ///
-  /// On Android, the initial route can be set on the [initialRoute](/javadoc/io/flutter/embedding/android/FlutterActivity.NewEngineIntentBuilder.html#initialRoute-java.lang.String-)
-  /// method of the [FlutterActivity](/javadoc/io/flutter/embedding/android/FlutterActivity.html)'s
-  /// intent builder.
-  ///
-  /// On a standalone engine, see https://flutter.dev/docs/development/add-to-app/android/add-flutter-screen#initial-route-with-a-cached-engine.
-  ///
-  /// ## iOS
-  ///
-  /// On iOS, the initial route can be set on the `initialRoute`
-  /// parameter of the [FlutterViewController](/objcdoc/Classes/FlutterViewController.html)'s
-  /// initializer.
-  ///
-  /// On a standalone engine, see https://flutter.dev/docs/development/add-to-app/ios/add-flutter-screen#route.
-  ///
-  /// See also:
-  ///
-  ///  * [Navigator], a widget that handles routing.
-  ///  * [SystemChannels.navigation], which handles subsequent navigation
-  ///    requests from the embedder.
   String get defaultRouteName => platformDispatcher.defaultRouteName;
 
-  /// Requests that, at the next appropriate opportunity, the [onBeginFrame] and
-  /// [onDrawFrame] callbacks be invoked.
-  ///
-  /// {@template dart.ui.window.functionForwardWarning}
-  /// Calling this function forwards the call to the same function on the
-  /// [PlatformDispatcher] singleton, so instead of calling it here, you should
-  /// consider calling it on `WidgetsBinding.instance.platformDispatcher` instead (or, when
-  /// `WidgetsBinding` isn't available, on [PlatformDispatcher.instance]). The
-  /// reason this function forwards to the [PlatformDispatcher] is to provide
-  /// convenience for applications that only use a single main window.
-  /// {@endtemplate}
-  ///
-  /// See also:
-  ///
-  /// * [SchedulerBinding], the Flutter framework class which manages the
-  ///   scheduling of frames.
   void scheduleFrame() => platformDispatcher.scheduleFrame();
 
-  /// Whether the user has requested that [updateSemantics] be called when
-  /// the semantic contents of window changes.
-  ///
-  /// {@macro dart.ui.window.accessorForwardWarning}
-  ///
-  /// The [onSemanticsEnabledChanged] callback is called whenever this value
-  /// changes.
   bool get semanticsEnabled => platformDispatcher.semanticsEnabled;
 
-  /// A callback that is invoked when the value of [semanticsEnabled] changes.
-  ///
-  /// {@macro dart.ui.window.accessorForwardWarning}
-  ///
-  /// The framework invokes this callback in the same zone in which the
-  /// callback was set.
   VoidCallback? get onSemanticsEnabledChanged => platformDispatcher.onSemanticsEnabledChanged;
   set onSemanticsEnabledChanged(VoidCallback? callback) {
     platformDispatcher.onSemanticsEnabledChanged = callback;
   }
 
-  /// A callback that is invoked whenever the user requests an action to be
-  /// performed.
-  ///
-  /// {@macro dart.ui.window.accessorForwardWarning}
-  ///
-  /// This callback is used when the user expresses the action they wish to
-  /// perform based on the semantics supplied by [updateSemantics].
-  ///
-  /// The framework invokes this callback in the same zone in which the
-  /// callback was set.
   SemanticsActionCallback? get onSemanticsAction => platformDispatcher.onSemanticsAction;
   set onSemanticsAction(SemanticsActionCallback? callback) {
     platformDispatcher.onSemanticsAction = callback;
   }
 
-  /// Additional accessibility features that may be enabled by the platform.
   AccessibilityFeatures get accessibilityFeatures => platformDispatcher.accessibilityFeatures;
 
-  /// A callback that is invoked when the value of [accessibilityFeatures] changes.
-  ///
-  /// {@macro dart.ui.window.accessorForwardWarning}
-  ///
-  /// The framework invokes this callback in the same zone in which the
-  /// callback was set.
   VoidCallback? get onAccessibilityFeaturesChanged => platformDispatcher.onAccessibilityFeaturesChanged;
   set onAccessibilityFeaturesChanged(VoidCallback? callback) {
     platformDispatcher.onAccessibilityFeaturesChanged = callback;
   }
 
-  /// Change the retained semantics data about this window.
-  ///
-  /// {@macro dart.ui.window.functionForwardWarning}
-  ///
-  /// If [semanticsEnabled] is true, the user has requested that this function
-  /// be called whenever the semantic content of this window changes.
-  ///
-  /// In either case, this function disposes the given update, which means the
-  /// semantics update cannot be used further.
   @override
   void updateSemantics(SemanticsUpdate update) => platformDispatcher.updateSemantics(update);
 
-  /// Sends a message to a platform-specific plugin.
-  ///
-  /// {@macro dart.ui.window.functionForwardWarning}
-  ///
-  /// The `name` parameter determines which plugin receives the message. The
-  /// `data` parameter contains the message payload and is typically UTF-8
-  /// encoded JSON but can be arbitrary data. If the plugin replies to the
-  /// message, `callback` will be called with the response.
-  ///
-  /// The framework invokes [callback] in the same zone in which this method
-  /// was called.
   void sendPlatformMessage(String name,
       ByteData? data,
       PlatformMessageResponseCallback? callback) {
     platformDispatcher.sendPlatformMessage(name, data, callback);
   }
 
-  /// Called whenever this window receives a message from a platform-specific
-  /// plugin.
-  ///
-  /// {@macro dart.ui.window.accessorForwardWarning}
-  ///
-  /// The `name` parameter determines which plugin sent the message. The `data`
-  /// parameter is the payload and is typically UTF-8 encoded JSON but can be
-  /// arbitrary data.
-  ///
-  /// Message handlers must call the function given in the `callback` parameter.
-  /// If the handler does not need to respond, the handler should pass null to
-  /// the callback.
-  ///
-  /// The framework invokes this callback in the same zone in which the
-  /// callback was set.
-  // TODO(ianh): deprecate once framework uses [ChannelBuffers.setListener].
   PlatformMessageCallback? get onPlatformMessage => platformDispatcher.onPlatformMessage;
   set onPlatformMessage(PlatformMessageCallback? callback) {
     platformDispatcher.onPlatformMessage = callback;
   }
 
-  /// Set the debug name associated with this platform dispatcher's root
-  /// isolate.
-  ///
-  /// {@macro dart.ui.window.accessorForwardWarning}
-  ///
-  /// Normally debug names are automatically generated from the Dart port, entry
-  /// point, and source file. For example: `main.dart$main-1234`.
-  ///
-  /// This can be combined with flutter tools `--isolate-filter` flag to debug
-  /// specific root isolates. For example: `flutter attach --isolate-filter=[name]`.
-  /// Note that this does not rename any child isolates of the root.
   void setIsolateDebugName(String name) => PlatformDispatcher.instance.setIsolateDebugName(name);
 }
 
diff --git a/framework/lib/material.dart b/framework/lib/material.dart
index fb07102..63901d8 100644
--- a/framework/lib/material.dart
+++ b/framework/lib/material.dart
@@ -21,7 +21,9 @@
 library material;
 
 export 'src/material/about.dart';
+export 'src/material/action_buttons.dart';
 export 'src/material/action_chip.dart';
+export 'src/material/action_icons_theme.dart';
 export 'src/material/adaptive_text_selection_toolbar.dart';
 export 'src/material/animated_icons.dart';
 export 'src/material/app.dart';
@@ -29,7 +31,6 @@
 export 'src/material/app_bar_theme.dart';
 export 'src/material/arc.dart';
 export 'src/material/autocomplete.dart';
-export 'src/material/back_button.dart';
 export 'src/material/badge.dart';
 export 'src/material/badge_theme.dart';
 export 'src/material/banner.dart';
diff --git a/framework/lib/src/cupertino/app.dart b/framework/lib/src/cupertino/app.dart
index dab2763..50b040d 100644
--- a/framework/lib/src/cupertino/app.dart
+++ b/framework/lib/src/cupertino/app.dart
@@ -558,6 +558,7 @@
         restorationScopeId: widget.restorationScopeId,
       );
     }
+
     return WidgetsApp(
       key: GlobalObjectKey(this),
       navigatorKey: widget.navigatorKey,
@@ -595,7 +596,7 @@
 
   @override
   Widget build(BuildContext context) {
-    final CupertinoThemeData effectiveThemeData = widget.theme ?? const CupertinoThemeData();
+    final CupertinoThemeData effectiveThemeData = (widget.theme ?? const CupertinoThemeData()).resolveFrom(context);
 
     return ScrollConfiguration(
       behavior: widget.scrollBehavior ?? const CupertinoScrollBehavior(),
diff --git a/framework/lib/src/cupertino/context_menu.dart b/framework/lib/src/cupertino/context_menu.dart
index 1fca184..acb7f74 100644
--- a/framework/lib/src/cupertino/context_menu.dart
+++ b/framework/lib/src/cupertino/context_menu.dart
@@ -277,7 +277,7 @@
   /// opened in the default way to match a native iOS 16.0 app. The behavior
   /// will match what will happen if the simple child image was passed as just
   /// the [child] parameter, instead of [builder]. This can be manipulated to
-  /// add more custamizability to the widget's animation.
+  /// add more customizability to the widget's animation.
   ///
   /// ```dart
   /// CupertinoContextMenu.builder(
diff --git a/framework/lib/src/cupertino/date_picker.dart b/framework/lib/src/cupertino/date_picker.dart
index 94522f1..7e20820 100644
--- a/framework/lib/src/cupertino/date_picker.dart
+++ b/framework/lib/src/cupertino/date_picker.dart
@@ -275,6 +275,7 @@
     this.use24hFormat = false,
     this.dateOrder,
     this.backgroundColor,
+    this.showDayOfWeek = false
   }) : initialDateTime = initialDateTime ?? DateTime.now(),
        assert(
          minuteInterval > 0 && 60 % minuteInterval == 0,
@@ -384,6 +385,9 @@
   /// Defaults to null, which disables background painting entirely.
   final Color? backgroundColor;
 
+  /// Whether to to show day of week alongside day. Defaults to false.
+  final bool showDayOfWeek;
+
   @override
   State<StatefulWidget> createState() { // ignore: no_logic_in_create_state, https://github.com/flutter/flutter/issues/70499
     // The `time` mode and `dateAndTime` mode of the picker share the time
@@ -404,6 +408,7 @@
     _PickerColumnType columnType,
     CupertinoLocalizations localizations,
     BuildContext context,
+    bool showDayOfWeek
   ) {
     String longestText = '';
 
@@ -443,10 +448,20 @@
             : localizations.postMeridiemAbbreviation;
         break;
       case _PickerColumnType.dayOfMonth:
+        int longestDayOfMonth = 1;
         for (int i = 1; i <=31; i++) {
           final String dayOfMonth = localizations.datePickerDayOfMonth(i);
           if (longestText.length < dayOfMonth.length) {
             longestText = dayOfMonth;
+            longestDayOfMonth = i;
+          }
+        }
+        if (showDayOfWeek) {
+          for (int wd = 1; wd < DateTime.daysPerWeek; wd++) {
+            final String dayOfMonth = localizations.datePickerDayOfMonth(longestDayOfMonth, wd);
+            if (longestText.length < dayOfMonth.length) {
+              longestText = dayOfMonth;
+            }
           }
         }
         break;
@@ -649,7 +664,7 @@
   double _getEstimatedColumnWidth(_PickerColumnType columnType) {
     if (estimatedColumnWidths[columnType.index] == null) {
       estimatedColumnWidths[columnType.index] =
-          CupertinoDatePicker._getColumnWidth(columnType, localizations, context);
+          CupertinoDatePicker._getColumnWidth(columnType, localizations, context, widget.showDayOfWeek);
     }
 
     return estimatedColumnWidths[columnType.index]!;
@@ -1151,9 +1166,9 @@
   }
 
   void _refreshEstimatedColumnWidths() {
-    estimatedColumnWidths[_PickerColumnType.dayOfMonth.index] = CupertinoDatePicker._getColumnWidth(_PickerColumnType.dayOfMonth, localizations, context);
-    estimatedColumnWidths[_PickerColumnType.month.index] = CupertinoDatePicker._getColumnWidth(_PickerColumnType.month, localizations, context);
-    estimatedColumnWidths[_PickerColumnType.year.index] = CupertinoDatePicker._getColumnWidth(_PickerColumnType.year, localizations, context);
+    estimatedColumnWidths[_PickerColumnType.dayOfMonth.index] = CupertinoDatePicker._getColumnWidth(_PickerColumnType.dayOfMonth, localizations, context, widget.showDayOfWeek);
+    estimatedColumnWidths[_PickerColumnType.month.index] = CupertinoDatePicker._getColumnWidth(_PickerColumnType.month, localizations, context, widget.showDayOfWeek);
+    estimatedColumnWidths[_PickerColumnType.year.index] = CupertinoDatePicker._getColumnWidth(_PickerColumnType.year, localizations, context, widget.showDayOfWeek);
   }
 
   // The DateTime of the last day of a given month in a given year.
@@ -1191,10 +1206,11 @@
         selectionOverlay: selectionOverlay,
         children: List<Widget>.generate(31, (int index) {
           final int day = index + 1;
+          final  int? dayOfWeek = widget.showDayOfWeek ? DateTime(selectedYear, selectedMonth, day).weekday : null;
           return itemPositioningBuilder(
             context,
             Text(
-              localizations.datePickerDayOfMonth(day),
+              localizations.datePickerDayOfMonth(day, dayOfWeek),
               style: _themeTextStyle(context, isValid: day <= daysInCurrentMonth),
             ),
           );
diff --git a/framework/lib/src/cupertino/localizations.dart b/framework/lib/src/cupertino/localizations.dart
index 77af039..ccb038a 100644
--- a/framework/lib/src/cupertino/localizations.dart
+++ b/framework/lib/src/cupertino/localizations.dart
@@ -82,12 +82,17 @@
   /// Day of month that is shown in [CupertinoDatePicker] spinner corresponding
   /// to the given day index.
   ///
+  /// If weekDay is provided then it will also show weekday name alongside the numerical day.
+  ///
   /// Examples: datePickerDayOfMonth(1) in:
   ///
   ///  - US English: 1
   ///  - Korean: 1일
+  /// Examples: datePickerDayOfMonth(1, 1) in:
+  ///
+  ///  - US English: Mon 1
   // The global version uses date symbols data from the intl package.
-  String datePickerDayOfMonth(int dayIndex);
+  String datePickerDayOfMonth(int dayIndex, [int? weekDay]);
 
   /// The medium-width date format that is shown in [CupertinoDatePicker]
   /// spinner. Abbreviates month and days of week.
@@ -292,7 +297,8 @@
   /// function, rather than constructing this class directly.
   const DefaultCupertinoLocalizations();
 
-  static const List<String> _shortWeekdays = <String>[
+  /// Short version of days of week.
+  static const List<String> shortWeekdays = <String>[
     'Mon',
     'Tue',
     'Wed',
@@ -341,7 +347,13 @@
   String datePickerMonth(int monthIndex) => _months[monthIndex - 1];
 
   @override
-  String datePickerDayOfMonth(int dayIndex) => dayIndex.toString();
+  String datePickerDayOfMonth(int dayIndex, [int? weekDay]) {
+    if (weekDay != null) {
+      return ' ${shortWeekdays[weekDay - DateTime.monday]} $dayIndex ';
+    }
+
+    return dayIndex.toString();
+  }
 
   @override
   String datePickerHour(int hour) => hour.toString();
@@ -362,7 +374,7 @@
 
   @override
   String datePickerMediumDate(DateTime date) {
-    return '${_shortWeekdays[date.weekday - DateTime.monday]} '
+    return '${shortWeekdays[date.weekday - DateTime.monday]} '
       '${_shortMonths[date.month - DateTime.january]} '
       '${date.day.toString().padRight(2)}';
   }
diff --git a/framework/lib/src/cupertino/magnifier.dart b/framework/lib/src/cupertino/magnifier.dart
index 05a92b9..1541f5e 100644
--- a/framework/lib/src/cupertino/magnifier.dart
+++ b/framework/lib/src/cupertino/magnifier.dart
@@ -77,7 +77,7 @@
 
 class _CupertinoTextMagnifierState extends State<CupertinoTextMagnifier>
     with SingleTickerProviderStateMixin {
-  // Initalize to dummy values for the event that the inital call to
+  // Initialize to dummy values for the event that the initial call to
   // _determineMagnifierPositionAndFocalPoint calls hide, and thus does not
   // set these values.
   Offset _currentAdjustedMagnifierPosition = Offset.zero;
diff --git a/framework/lib/src/cupertino/nav_bar.dart b/framework/lib/src/cupertino/nav_bar.dart
index 6d73f55..648f4c8 100644
--- a/framework/lib/src/cupertino/nav_bar.dart
+++ b/framework/lib/src/cupertino/nav_bar.dart
@@ -897,7 +897,7 @@
         titleTextStyle: CupertinoTheme.of(context).textTheme.navTitleTextStyle,
         largeTitleTextStyle: CupertinoTheme.of(context).textTheme.navLargeTitleTextStyle,
         border: border,
-        hasUserMiddle: userMiddle != null,
+        hasUserMiddle: userMiddle != null && (alwaysShowMiddle || !showLargeTitle),
         largeExpanded: showLargeTitle,
         child: navBar,
       ),
@@ -967,8 +967,8 @@
       return;
     }
 
-    final BoxConstraints childConstriants = constraints.widthConstraints().loosen();
-    child.layout(childConstriants, parentUsesSize: true);
+    final BoxConstraints childConstraints = constraints.widthConstraints().loosen();
+    child.layout(childConstraints, parentUsesSize: true);
 
     final double maxScale = child.size.width != 0.0
       ? clampDouble(constraints.maxWidth / child.size.width, 1.0, 1.1)
diff --git a/framework/lib/src/cupertino/refresh.dart b/framework/lib/src/cupertino/refresh.dart
index 3ada10c..a774e4a 100644
--- a/framework/lib/src/cupertino/refresh.dart
+++ b/framework/lib/src/cupertino/refresh.dart
@@ -398,7 +398,7 @@
     switch (refreshState) {
       case RefreshIndicatorMode.drag:
         // While we're dragging, we draw individual ticks of the spinner while simultaneously
-        // easing the opacity in. Note that the opacity curve values here were derived using
+        // easing the opacity in. The opacity curve values here were derived using
         // Xcode through inspecting a native app running on iOS 13.5.
         const Curve opacityCurve = Interval(0.0, 0.35, curve: Curves.easeInOut);
         return Opacity(
diff --git a/framework/lib/src/cupertino/route.dart b/framework/lib/src/cupertino/route.dart
index bb158ad..9576047 100644
--- a/framework/lib/src/cupertino/route.dart
+++ b/framework/lib/src/cupertino/route.dart
@@ -835,8 +835,8 @@
     _CupertinoEdgeShadowDecoration? b,
     double t,
   ) {
-    if (a == null && b == null) {
-      return null;
+    if (identical(a, b)) {
+      return a;
     }
     if (a == null) {
       return b!._colors == null ? b : _CupertinoEdgeShadowDecoration._(b._colors!.map<Color>((Color color) => Color.lerp(null, color, t)!).toList());
diff --git a/framework/lib/src/cupertino/switch.dart b/framework/lib/src/cupertino/switch.dart
index dedaf4e..ef48a4c 100644
--- a/framework/lib/src/cupertino/switch.dart
+++ b/framework/lib/src/cupertino/switch.dart
@@ -364,7 +364,7 @@
             activeColor: activeColor,
             trackColor: CupertinoDynamicColor.resolve(widget.trackColor ?? CupertinoColors.secondarySystemFill, context),
             thumbColor: CupertinoDynamicColor.resolve(widget.thumbColor ?? CupertinoColors.white, context),
-            // Opacity, lightness, and saturation values were aproximated with
+            // Opacity, lightness, and saturation values were approximated with
             // color pickers on the switches in the macOS settings.
             focusColor: CupertinoDynamicColor.resolve(
               widget.focusColor ??
diff --git a/framework/lib/src/cupertino/text_field.dart b/framework/lib/src/cupertino/text_field.dart
index a8ec7e6..2a52d27 100644
--- a/framework/lib/src/cupertino/text_field.dart
+++ b/framework/lib/src/cupertino/text_field.dart
@@ -996,8 +996,7 @@
       case TargetPlatform.windows:
       case TargetPlatform.fuchsia:
       case TargetPlatform.android:
-        if (cause == SelectionChangedCause.longPress
-            || cause == SelectionChangedCause.drag) {
+        if (cause == SelectionChangedCause.longPress) {
           _editableText.bringIntoView(selection.extent);
         }
         break;
diff --git a/framework/lib/src/foundation/assertions.dart b/framework/lib/src/foundation/assertions.dart
index 59abde7..188d981 100644
--- a/framework/lib/src/foundation/assertions.dart
+++ b/framework/lib/src/foundation/assertions.dart
@@ -223,6 +223,13 @@
          level: level,
        );
 
+  @override
+  String toString({
+    TextTreeConfiguration? parentConfiguration,
+    DiagnosticLevel minLevel = DiagnosticLevel.info,
+  }) {
+    return valueToString(parentConfiguration: parentConfiguration);
+  }
 
   @override
   List<Object> get value => super.value!;
diff --git a/framework/lib/src/foundation/binding.dart b/framework/lib/src/foundation/binding.dart
index 97e30cb..359315b 100644
--- a/framework/lib/src/foundation/binding.dart
+++ b/framework/lib/src/foundation/binding.dart
@@ -146,7 +146,7 @@
       return true;
     }());
 
-    assert(_debugInitializedType == null);
+    assert(_debugInitializedType == null, 'Binding is already initialized to $_debugInitializedType');
     initInstances();
     assert(_debugInitializedType != null);
 
diff --git a/framework/lib/src/foundation/memory_allocations.dart b/framework/lib/src/foundation/memory_allocations.dart
index 1323f2b..5084b57 100644
--- a/framework/lib/src/foundation/memory_allocations.dart
+++ b/framework/lib/src/foundation/memory_allocations.dart
@@ -39,7 +39,7 @@
   /// long living place as it will prevent garbage collection.
   final Object object;
 
-  /// The representation of the event in a form, acceptible by a
+  /// The representation of the event in a form, acceptable by a
   /// pure dart library, that cannot depend on Flutter.
   ///
   /// The method enables code like:
diff --git a/framework/lib/src/gestures/gesture_settings.dart b/framework/lib/src/gestures/gesture_settings.dart
index afaa5ed..f701ef3 100644
--- a/framework/lib/src/gestures/gesture_settings.dart
+++ b/framework/lib/src/gestures/gesture_settings.dart
@@ -26,7 +26,7 @@
 
   /// Create a new [DeviceGestureSettings] from the provided [view].
   factory DeviceGestureSettings.fromView(ui.FlutterView view) {
-    final double? physicalTouchSlop = view.viewConfiguration.gestureSettings.physicalTouchSlop;
+    final double? physicalTouchSlop = view.gestureSettings.physicalTouchSlop;
     return DeviceGestureSettings(
       touchSlop: physicalTouchSlop == null ? null : physicalTouchSlop / view.devicePixelRatio
     );
diff --git a/framework/lib/src/gestures/monodrag.dart b/framework/lib/src/gestures/monodrag.dart
index 4930910..6617d9a 100644
--- a/framework/lib/src/gestures/monodrag.dart
+++ b/framework/lib/src/gestures/monodrag.dart
@@ -222,6 +222,24 @@
   late OffsetPair _initialPosition;
   late OffsetPair _pendingDragOffset;
   Duration? _lastPendingEventTimestamp;
+
+  /// When asserts are enabled, returns the last tracked pending event timestamp
+  /// for this recognizer.
+  ///
+  /// Otherwise, returns null.
+  ///
+  /// This getter is intended for use in framework unit tests. Applications must
+  /// not depend on its value.
+  @visibleForTesting
+  Duration? get debugLastPendingEventTimestamp {
+    Duration? lastPendingEventTimestamp;
+    assert(() {
+      lastPendingEventTimestamp = _lastPendingEventTimestamp;
+      return true;
+    }());
+    return lastPendingEventTimestamp;
+  }
+
   // The buttons sent by `PointerDownEvent`. If a `PointerMoveEvent` comes with a
   // different set of buttons, the gesture is canceled.
   int? _initialButtons;
@@ -363,7 +381,7 @@
     if (_state != _DragState.accepted) {
       _state = _DragState.accepted;
       final OffsetPair delta = _pendingDragOffset;
-      final Duration timestamp = _lastPendingEventTimestamp!;
+      final Duration? timestamp = _lastPendingEventTimestamp;
       final Matrix4? transform = _lastTransform;
       final Offset localUpdateDelta;
       switch (dragStartBehavior) {
@@ -449,7 +467,7 @@
     }
   }
 
-  void _checkStart(Duration timestamp, int pointer) {
+  void _checkStart(Duration? timestamp, int pointer) {
     if (onStart != null) {
       final DragStartDetails details = DragStartDetails(
         sourceTimeStamp: timestamp,
diff --git a/framework/lib/src/gestures/resampler.dart b/framework/lib/src/gestures/resampler.dart
index aa36725..e2a67e0 100644
--- a/framework/lib/src/gestures/resampler.dart
+++ b/framework/lib/src/gestures/resampler.dart
@@ -240,7 +240,8 @@
       // generated when the position has changed.
       if (event is! PointerMoveEvent && event is! PointerHoverEvent) {
         // Add synthetics `move` or `hover` event if position has changed.
-        // Note: Devices without `hover` events are expected to always have
+        //
+        // Devices without `hover` events are expected to always have
         // `add` and `down` events with the same position and this logic will
         // therefore never produce `hover` events.
         if (position != _position) {
diff --git a/framework/lib/src/material/about.dart b/framework/lib/src/material/about.dart
index 0b26bd2..ea3a336 100644
--- a/framework/lib/src/material/about.dart
+++ b/framework/lib/src/material/about.dart
@@ -463,11 +463,11 @@
 
   Widget _packagesView(final BuildContext _, final bool isLateral) {
     final Widget about = _AboutProgram(
-        name: widget.applicationName ?? _defaultApplicationName(context),
-        icon: widget.applicationIcon ?? _defaultApplicationIcon(context),
-        version: widget.applicationVersion ?? _defaultApplicationVersion(context),
-        legalese: widget.applicationLegalese,
-      );
+      name: widget.applicationName ?? _defaultApplicationName(context),
+      icon: widget.applicationIcon ?? _defaultApplicationIcon(context),
+      version: widget.applicationVersion ?? _defaultApplicationVersion(context),
+      legalese: widget.applicationLegalese,
+    );
     return _PackagesView(
       about: about,
       isLateral: isLateral,
@@ -870,10 +870,11 @@
       page = Scaffold(
         appBar: AppBar(
           title: _PackageLicensePageTitle(
-            title,
-            subtitle,
-            theme.primaryTextTheme,
-            theme.appBarTheme.titleTextStyle,
+            title: title,
+            subtitle: subtitle,
+            theme: theme.useMaterial3 ? theme.textTheme : theme.primaryTextTheme,
+            titleTextStyle: theme.appBarTheme.titleTextStyle,
+            foregroundColor: theme.appBarTheme.foregroundColor,
           ),
         ),
         body: Center(
@@ -909,7 +910,12 @@
             automaticallyImplyLeading: false,
             pinned: true,
             backgroundColor: theme.cardColor,
-            title: _PackageLicensePageTitle(title, subtitle, theme.textTheme, theme.textTheme.titleLarge),
+            title: _PackageLicensePageTitle(
+              title: title,
+              subtitle: subtitle,
+              theme: theme.textTheme,
+              titleTextStyle: theme.textTheme.titleLarge,
+            ),
           ),
           SliverPadding(
             padding: padding,
@@ -935,29 +941,29 @@
 }
 
 class _PackageLicensePageTitle extends StatelessWidget {
-  const _PackageLicensePageTitle(
-    this.title,
-    this.subtitle,
-    this.theme,
+  const _PackageLicensePageTitle({
+    required this.title,
+    required this.subtitle,
+    required this.theme,
     this.titleTextStyle,
-  );
+    this.foregroundColor,
+  });
 
   final String title;
   final String subtitle;
   final TextTheme theme;
   final TextStyle? titleTextStyle;
+  final Color? foregroundColor;
 
   @override
   Widget build(BuildContext context) {
-    final Color? color = Theme.of(context).appBarTheme.foregroundColor;
     final TextStyle? effectiveTitleTextStyle = titleTextStyle ?? theme.titleLarge;
-
     return Column(
       mainAxisAlignment: MainAxisAlignment.center,
       crossAxisAlignment: CrossAxisAlignment.start,
       children: <Widget>[
-        Text(title, style: effectiveTitleTextStyle?.copyWith(color: color)),
-        Text(subtitle, style: theme.titleSmall?.copyWith(color: color)),
+        Text(title, style: effectiveTitleTextStyle?.copyWith(color: foregroundColor)),
+        Text(subtitle, style: theme.titleSmall?.copyWith(color: foregroundColor)),
       ],
     );
   }
@@ -1227,16 +1233,18 @@
 
   MaterialPageRoute<void> _masterPageRoute(BuildContext context) {
     return MaterialPageRoute<dynamic>(
-      builder: (BuildContext c) => BlockSemantics(
-        child: _MasterPage(
-                leading: widget.automaticallyImplyLeading && Navigator.of(context).canPop()
-                        ? BackButton(onPressed: () => Navigator.of(context).pop())
-                        : null,
-                title: widget.title,
-                automaticallyImplyLeading: widget.automaticallyImplyLeading,
-                masterViewBuilder: widget.masterViewBuilder,
-              ),
-      ),
+      builder: (BuildContext c) {
+        return BlockSemantics(
+          child: _MasterPage(
+            leading: widget.automaticallyImplyLeading && Navigator.of(context).canPop()
+              ? BackButton(onPressed: () { Navigator.of(context).pop(); })
+              : null,
+            title: widget.title,
+            automaticallyImplyLeading: widget.automaticallyImplyLeading,
+            masterViewBuilder: widget.masterViewBuilder,
+          ),
+        );
+      },
     );
   }
 
@@ -1285,14 +1293,14 @@
   @override
   Widget build(BuildContext context) {
     return Scaffold(
-        appBar: AppBar(
-          title: title,
-          leading: leading,
-          actions: const <Widget>[],
-          automaticallyImplyLeading: automaticallyImplyLeading,
-        ),
-        body: masterViewBuilder!(context, false),
-      );
+      appBar: AppBar(
+        title: title,
+        leading: leading,
+        actions: const <Widget>[],
+        automaticallyImplyLeading: automaticallyImplyLeading,
+      ),
+      body: masterViewBuilder!(context, false),
+    );
   }
 
 }
@@ -1400,7 +1408,10 @@
               ),
             ),
           ),
-          body: _masterPanel(context),
+          body: Align(
+            alignment: AlignmentDirectional.centerStart,
+            child: _masterPanel(context),
+          ),
         ),
         // Detail view stacked above main scaffold and master view.
         SafeArea(
diff --git a/framework/lib/src/material/action_buttons.dart b/framework/lib/src/material/action_buttons.dart
new file mode 100644
index 0000000..6e0e72d
--- /dev/null
+++ b/framework/lib/src/material/action_buttons.dart
@@ -0,0 +1,418 @@
+// 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:flute/foundation.dart';
+import 'package:flute/widgets.dart';
+
+import 'action_icons_theme.dart';
+import 'button_style.dart';
+import 'debug.dart';
+import 'icon_button.dart';
+import 'icons.dart';
+import 'material_localizations.dart';
+import 'scaffold.dart';
+import 'theme.dart';
+
+abstract class _ActionButton extends StatelessWidget {
+  /// Creates a Material Design icon button.
+  const _ActionButton({
+    super.key,
+    this.color,
+    required this.icon,
+    required this.onPressed,
+    this.style,
+  });
+
+  /// The icon to display inside the button.
+  final Widget icon;
+
+  /// The callback that is called when the button is tapped
+  /// or otherwise activated.
+  ///
+  /// If this is set to null, the button will do a default action
+  /// when it is tapped or activated.
+  final VoidCallback? onPressed;
+
+  /// The color to use for the icon.
+  ///
+  /// Defaults to the [IconThemeData.color] specified in the ambient [IconTheme],
+  /// which usually matches the ambient [Theme]'s [ThemeData.iconTheme].
+  final Color? color;
+
+  /// Customizes this icon button's appearance.
+  ///
+  /// The [style] is only used for Material 3 [IconButton]s. If [ThemeData.useMaterial3]
+  /// is set to true, [style] is preferred for icon button customization, and any
+  /// parameters defined in [style] will override the same parameters in [IconButton].
+  ///
+  /// Null by default.
+  final ButtonStyle? style;
+
+  /// This returns the appropriate tooltip text for this action button.
+  String _getTooltip(BuildContext context);
+
+  /// This is the default function that is called when [onPressed] is set
+  /// to null.
+  void _onPressedCallback(BuildContext context);
+
+  @override
+  Widget build(BuildContext context) {
+    assert(debugCheckHasMaterialLocalizations(context));
+    return IconButton(
+      icon: icon,
+      style: style,
+      color: color,
+      tooltip: _getTooltip(context),
+      onPressed: () {
+        if (onPressed != null) {
+          onPressed!();
+        } else {
+          _onPressedCallback(context);
+        }
+      },
+    );
+  }
+}
+
+typedef _ActionIconBuilderCallback = WidgetBuilder? Function(ActionIconThemeData? actionIconTheme);
+typedef _ActionIconDataCallback = IconData Function(BuildContext context);
+typedef _AndroidSemanticsLabelCallback = String Function(MaterialLocalizations materialLocalization);
+
+class _ActionIcon extends StatelessWidget {
+  const _ActionIcon({
+    required this.iconBuilderCallback,
+    required this.getIcon,
+    required this.getAndroidSemanticsLabel,
+  });
+
+  final _ActionIconBuilderCallback iconBuilderCallback;
+  final _ActionIconDataCallback getIcon;
+  final _AndroidSemanticsLabelCallback getAndroidSemanticsLabel;
+
+  @override
+  Widget build(BuildContext context) {
+    final ActionIconThemeData? actionIconTheme = ActionIconTheme.of(context);
+    final WidgetBuilder? iconBuilder = iconBuilderCallback(actionIconTheme);
+    if (iconBuilder != null) {
+      return iconBuilder(context);
+    }
+
+    final IconData data = getIcon(context);
+    final String? semanticsLabel;
+    // This can't use the platform from Theme because it is the Android OS that
+    // expects the duplicated tooltip and label.
+    switch (defaultTargetPlatform) {
+      case TargetPlatform.android:
+        semanticsLabel = getAndroidSemanticsLabel(MaterialLocalizations.of(context));
+        break;
+      case TargetPlatform.fuchsia:
+      case TargetPlatform.linux:
+      case TargetPlatform.windows:
+      case TargetPlatform.iOS:
+      case TargetPlatform.macOS:
+        semanticsLabel = null;
+        break;
+    }
+
+    return Icon(data, semanticLabel: semanticsLabel);
+  }
+}
+
+/// A "back" icon that's appropriate for the current [TargetPlatform].
+///
+/// The current platform is determined by querying for the ambient [Theme].
+///
+/// See also:
+///
+///  * [BackButton], an [IconButton] with a [BackButtonIcon] that calls
+///    [Navigator.maybePop] to return to the previous route.
+///  * [IconButton], which is a more general widget for creating buttons
+///    with icons.
+///  * [Icon], a Material Design icon.
+///  * [ThemeData.platform], which specifies the current platform.
+class BackButtonIcon extends StatelessWidget {
+  /// Creates an icon that shows the appropriate "back" image for
+  /// the current platform (as obtained from the [Theme]).
+  const BackButtonIcon({ super.key });
+
+  @override
+  Widget build(BuildContext context) {
+    return _ActionIcon(
+      iconBuilderCallback: (ActionIconThemeData? actionIconTheme) {
+        return actionIconTheme?.backButtonIconBuilder;
+      },
+      getIcon: (BuildContext context) {
+        if (kIsWeb) {
+          // Always use 'Icons.arrow_back' as a back_button icon in web.
+          return Icons.arrow_back;
+        }
+        switch (Theme.of(context).platform) {
+          case TargetPlatform.android:
+          case TargetPlatform.fuchsia:
+          case TargetPlatform.linux:
+          case TargetPlatform.windows:
+            return Icons.arrow_back;
+          case TargetPlatform.iOS:
+          case TargetPlatform.macOS:
+            return Icons.arrow_back_ios;
+        }
+      },
+      getAndroidSemanticsLabel: (MaterialLocalizations materialLocalization) {
+        return materialLocalization.backButtonTooltip;
+      },
+    );
+  }
+}
+
+/// A Material Design back icon button.
+///
+/// A [BackButton] is an [IconButton] with a "back" icon appropriate for the
+/// current [TargetPlatform]. When pressed, the back button calls
+/// [Navigator.maybePop] to return to the previous route unless a custom
+/// [onPressed] callback is provided.
+///
+/// The [onPressed] callback can, for instance, be used to pop the platform's navigation stack
+/// via [SystemNavigator] instead of Flutter's [Navigator] in add-to-app
+/// situations.
+///
+/// In Material Design 3, both [style]'s [ButtonStyle.iconColor] and [color] are
+/// used to override the default icon color of [BackButton]. If both exist, the [ButtonStyle.iconColor]
+/// will override [color] for states where [ButtonStyle.foregroundColor] resolves to non-null.
+///
+/// When deciding to display a [BackButton], consider using
+/// `ModalRoute.of(context)?.canPop` to check whether the current route can be
+/// popped. If that value is false (e.g., because the current route is the
+/// initial route), the [BackButton] will not have any effect when pressed,
+/// which could frustrate the user.
+///
+/// Requires one of its ancestors to be a [Material] widget.
+///
+/// See also:
+///
+///  * [AppBar], which automatically uses a [BackButton] in its
+///    [AppBar.leading] slot when the [Scaffold] has no [Drawer] and the
+///    current [Route] is not the [Navigator]'s first route.
+///  * [BackButtonIcon], which is useful if you need to create a back button
+///    that responds differently to being pressed.
+///  * [IconButton], which is a more general widget for creating buttons with
+///    icons.
+///  * [CloseButton], an alternative which may be more appropriate for leaf
+///    node pages in the navigation tree.
+class BackButton extends _ActionButton {
+  /// Creates an [IconButton] with the appropriate "back" icon for the current
+  /// target platform.
+  const BackButton({
+    super.key,
+    super.color,
+    super.style,
+    super.onPressed,
+  }) : super(icon: const BackButtonIcon());
+
+  @override
+  void _onPressedCallback(BuildContext context) => Navigator.maybePop(context);
+
+  @override
+  String _getTooltip(BuildContext context) {
+    return MaterialLocalizations.of(context).backButtonTooltip;
+  }
+}
+
+/// A "close" icon that's appropriate for the current [TargetPlatform].
+///
+/// The current platform is determined by querying for the ambient [Theme].
+///
+/// See also:
+///
+///  * [CloseButton], an [IconButton] with a [CloseButtonIcon] that calls
+///    [Navigator.maybePop] to return to the previous route.
+///  * [IconButton], which is a more general widget for creating buttons
+///    with icons.
+///  * [Icon], a Material Design icon.
+///  * [ThemeData.platform], which specifies the current platform.
+class CloseButtonIcon extends StatelessWidget {
+  /// Creates an icon that shows the appropriate "close" image for
+  /// the current platform (as obtained from the [Theme]).
+  const CloseButtonIcon({ super.key });
+
+  @override
+  Widget build(BuildContext context) {
+    return _ActionIcon(
+      iconBuilderCallback: (ActionIconThemeData? actionIconTheme) {
+        return actionIconTheme?.closeButtonIconBuilder;
+      },
+      getIcon: (BuildContext context) => Icons.close,
+      getAndroidSemanticsLabel: (MaterialLocalizations materialLocalization) {
+        return materialLocalization.closeButtonTooltip;
+      },
+    );
+  }
+}
+
+/// A Material Design close icon button.
+///
+/// A [CloseButton] is an [IconButton] with a "close" icon. When pressed, the
+/// close button calls [Navigator.maybePop] to return to the previous route.
+///
+/// The [onPressed] callback can, for instance, be used to pop the platform's navigation stack
+/// via [SystemNavigator] instead of Flutter's [Navigator] in add-to-app
+/// situations.
+///
+/// In Material Design 3, both [style]'s [ButtonStyle.iconColor] and [color] are
+/// used to override the default icon color of [CloseButton]. If both exist, the [ButtonStyle.iconColor]
+/// will override [color] for states where [ButtonStyle.foregroundColor] resolves to non-null.
+///
+/// Use a [CloseButton] instead of a [BackButton] on fullscreen dialogs or
+/// pages that may solicit additional actions to close.
+///
+/// See also:
+///
+///  * [AppBar], which automatically uses a [CloseButton] in its
+///    [AppBar.leading] slot when appropriate.
+///  * [BackButton], which is more appropriate for middle nodes in the
+///    navigation tree or where pages can be popped instantaneously with
+///    no user data consequence.
+///  * [IconButton], to create other Material Design icon buttons.
+class CloseButton extends _ActionButton {
+  /// Creates a Material Design close icon button.
+  const CloseButton({ super.key, super.color, super.onPressed, super.style })
+      : super(icon: const CloseButtonIcon());
+
+  @override
+  void _onPressedCallback(BuildContext context) => Navigator.maybePop(context);
+
+  @override
+  String _getTooltip(BuildContext context) {
+    return MaterialLocalizations.of(context).closeButtonTooltip;
+  }
+}
+
+/// A "drawer" icon that's appropriate for the current [TargetPlatform].
+///
+/// The current platform is determined by querying for the ambient [Theme].
+///
+/// See also:
+///
+///  * [DrawerButton], an [IconButton] with a [DrawerButtonIcon] that calls
+///    [ScaffoldState.openDrawer] to open the [Scaffold.drawer].
+///  * [EndDrawerButton], an [IconButton] with an [EndDrawerButtonIcon] that
+///    calls [ScaffoldState.openEndDrawer] to open the [Scaffold.endDrawer].
+///  * [IconButton], which is a more general widget for creating buttons
+///    with icons.
+///  * [Icon], a Material Design icon.
+///  * [ThemeData.platform], which specifies the current platform.
+class DrawerButtonIcon extends StatelessWidget {
+  /// Creates an icon that shows the appropriate "close" image for
+  /// the current platform (as obtained from the [Theme]).
+  const DrawerButtonIcon({ super.key });
+
+  @override
+  Widget build(BuildContext context) {
+    return _ActionIcon(
+      iconBuilderCallback: (ActionIconThemeData? actionIconTheme) {
+        return actionIconTheme?.drawerButtonIconBuilder;
+      },
+      getIcon: (BuildContext context) => Icons.menu,
+      getAndroidSemanticsLabel: (MaterialLocalizations materialLocalization) {
+        return materialLocalization.openAppDrawerTooltip;
+      },
+    );
+  }
+}
+
+/// A Material Design drawer icon button.
+///
+/// A [DrawerButton] is an [IconButton] with a "drawer" icon. When pressed, the
+/// close button calls [ScaffoldState.openDrawer] to the [Scaffold.drawer].
+///
+/// The default behaviour on press can be overriden with [onPressed].
+///
+/// See also:
+///
+///  * [EndDrawerButton], an [IconButton] with an [EndDrawerButtonIcon] that
+///    calls [ScaffoldState.openEndDrawer] to open the [Scaffold.endDrawer].
+///  * [IconButton], which is a more general widget for creating buttons
+///    with icons.
+///  * [Icon], a Material Design icon.
+///  * [ThemeData.platform], which specifies the current platform.
+class DrawerButton extends _ActionButton {
+  /// Creates a Material Design drawer icon button.
+  const DrawerButton({
+    super.key,
+    super.style,
+    super.onPressed,
+  }) : super(icon: const DrawerButtonIcon());
+
+  @override
+  void _onPressedCallback(BuildContext context) => Scaffold.of(context).openDrawer();
+
+  @override
+  String _getTooltip(BuildContext context) {
+    return MaterialLocalizations.of(context).openAppDrawerTooltip;
+  }
+}
+
+/// A "end drawer" icon that's appropriate for the current [TargetPlatform].
+///
+/// The current platform is determined by querying for the ambient [Theme].
+///
+/// See also:
+///
+///  * [DrawerButton], an [IconButton] with a [DrawerButtonIcon] that calls
+///    [ScaffoldState.openDrawer] to open the [Scaffold.drawer].
+///  * [EndDrawerButton], an [IconButton] with an [EndDrawerButtonIcon] that
+///    calls [ScaffoldState.openEndDrawer] to open the [Scaffold.endDrawer]
+///  * [IconButton], which is a more general widget for creating buttons
+///    with icons.
+///  * [Icon], a Material Design icon.
+///  * [ThemeData.platform], which specifies the current platform.
+class EndDrawerButtonIcon extends StatelessWidget {
+  /// Creates an icon that shows the appropriate "end drawer" image for
+  /// the current platform (as obtained from the [Theme]).
+  const EndDrawerButtonIcon({ super.key });
+
+  @override
+  Widget build(BuildContext context) {
+    return _ActionIcon(
+      iconBuilderCallback: (ActionIconThemeData? actionIconTheme) {
+        return actionIconTheme?.endDrawerButtonIconBuilder;
+      },
+      getIcon: (BuildContext context) => Icons.menu,
+      getAndroidSemanticsLabel: (MaterialLocalizations materialLocalization) {
+        return materialLocalization.openAppDrawerTooltip;
+      },
+    );
+  }
+}
+
+/// A Material Design end drawer icon button.
+///
+/// A [EndDrawerButton] is an [IconButton] with a "drawer" icon. When pressed, the
+/// end drawer button calls [ScaffoldState.openEndDrawer] to open the [Scaffold.endDrawer].
+///
+/// The default behaviour on press can be overriden with [onPressed].
+///
+/// See also:
+///
+///  * [DrawerButton], an [IconButton] with a [DrawerButtonIcon] that calls
+///    [ScaffoldState.openDrawer] to open a drawer.
+///  * [IconButton], which is a more general widget for creating buttons
+///    with icons.
+///  * [Icon], a Material Design icon.
+///  * [ThemeData.platform], which specifies the current platform.
+class EndDrawerButton extends _ActionButton {
+  /// Creates a Material Design end drawer icon button.
+  const EndDrawerButton({
+    super.key,
+    super.style,
+    super.onPressed,
+  }) : super(icon: const EndDrawerButtonIcon());
+
+  @override
+  void _onPressedCallback(BuildContext context) => Scaffold.of(context).openEndDrawer();
+
+  @override
+  String _getTooltip(BuildContext context) {
+    return MaterialLocalizations.of(context).openAppDrawerTooltip;
+  }
+}
diff --git a/framework/lib/src/material/action_icons_theme.dart b/framework/lib/src/material/action_icons_theme.dart
new file mode 100644
index 0000000..bfcbdd3
--- /dev/null
+++ b/framework/lib/src/material/action_icons_theme.dart
@@ -0,0 +1,153 @@
+// 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:flute/foundation.dart';
+import 'package:flute/widgets.dart';
+
+import 'action_buttons.dart';
+import 'theme.dart';
+
+// Examples can assume:
+// late BuildContext context;
+
+/// A [ActionIconThemeData] that overrides the default icons of
+/// [BackButton], [CloseButton], [DrawerButton], and [EndDrawerButton] with
+/// [ActionIconTheme.of] or the overall [Theme]'s [ThemeData.actionIconTheme].
+@immutable
+class ActionIconThemeData with Diagnosticable {
+  /// Creates an [ActionIconThemeData].
+  ///
+  /// The builders [backButtonIconBuilder], [closeButtonIconBuilder],
+  /// [drawerButtonIconBuilder], [endDrawerButtonIconBuilder] may be null.
+  const ActionIconThemeData({ this.backButtonIconBuilder, this.closeButtonIconBuilder, this.drawerButtonIconBuilder, this.endDrawerButtonIconBuilder });
+
+  /// Overrides [BackButtonIcon]'s icon.
+  ///
+  /// If [backButtonIconBuilder] is null, then [BackButtonIcon]
+  /// fallbacks to the platform's default back button icon.
+  final WidgetBuilder? backButtonIconBuilder;
+
+  /// Overrides [CloseButtonIcon]'s icon.
+  ///
+  /// If [closeButtonIconBuilder] is null, then [CloseButtonIcon]
+  /// fallbacks to the platform's default close button icon.
+  final WidgetBuilder? closeButtonIconBuilder;
+
+  /// Overrides [DrawerButtonIcon]'s icon.
+  ///
+  /// If [drawerButtonIconBuilder] is null, then [DrawerButtonIcon]
+  /// fallbacks to the platform's default drawer button icon.
+  final WidgetBuilder? drawerButtonIconBuilder;
+
+  /// Overrides [EndDrawerButtonIcon]'s icon.
+  ///
+  /// If [endDrawerButtonIconBuilder] is null, then [EndDrawerButtonIcon]
+  /// fallbacks to the platform's default end drawer button icon.
+  final WidgetBuilder? endDrawerButtonIconBuilder;
+
+  /// Creates a copy of this object but with the given fields replaced with the
+  /// new values.
+  ActionIconThemeData copyWith({
+    WidgetBuilder? backButtonIconBuilder,
+    WidgetBuilder? closeButtonIconBuilder,
+    WidgetBuilder? drawerButtonIconBuilder,
+    WidgetBuilder? endDrawerButtonIconBuilder,
+  }) {
+    return ActionIconThemeData(
+      backButtonIconBuilder: backButtonIconBuilder ?? backButtonIconBuilder,
+      closeButtonIconBuilder: closeButtonIconBuilder ?? closeButtonIconBuilder,
+      drawerButtonIconBuilder: drawerButtonIconBuilder ?? drawerButtonIconBuilder,
+      endDrawerButtonIconBuilder: endDrawerButtonIconBuilder ?? endDrawerButtonIconBuilder,
+    );
+  }
+
+  /// Linearly interpolate between two action icon themes.
+  static ActionIconThemeData? lerp(ActionIconThemeData? a, ActionIconThemeData? b, double t) {
+    if (a == null && b == null) {
+      return null;
+    }
+    return ActionIconThemeData(
+      backButtonIconBuilder: t < 0.5 ? a?.backButtonIconBuilder : b?.backButtonIconBuilder,
+      closeButtonIconBuilder: t < 0.5 ? a?.closeButtonIconBuilder : b?.closeButtonIconBuilder,
+      drawerButtonIconBuilder: t < 0.5 ? a?.drawerButtonIconBuilder : b?.drawerButtonIconBuilder,
+      endDrawerButtonIconBuilder: t < 0.5 ? a?.endDrawerButtonIconBuilder : b?.endDrawerButtonIconBuilder,
+    );
+  }
+
+  @override
+  int get hashCode {
+    final List<Object?> values = <Object?>[
+      backButtonIconBuilder,
+      closeButtonIconBuilder,
+      drawerButtonIconBuilder,
+      endDrawerButtonIconBuilder,
+    ];
+    return Object.hashAll(values);
+  }
+
+  @override
+  bool operator ==(Object other) {
+    if (identical(this, other)) {
+      return true;
+    }
+    if (other.runtimeType != runtimeType) {
+      return false;
+    }
+    return other is ActionIconThemeData
+        && other.backButtonIconBuilder == backButtonIconBuilder
+        && other.closeButtonIconBuilder == closeButtonIconBuilder
+        && other.drawerButtonIconBuilder == drawerButtonIconBuilder
+        && other.endDrawerButtonIconBuilder == endDrawerButtonIconBuilder;
+  }
+
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    properties.add(DiagnosticsProperty<WidgetBuilder>('backButtonIconBuilder', backButtonIconBuilder, defaultValue: null));
+    properties.add(DiagnosticsProperty<WidgetBuilder>('closeButtonIconBuilder', closeButtonIconBuilder, defaultValue: null));
+    properties.add(DiagnosticsProperty<WidgetBuilder>('drawerButtonIconBuilder', drawerButtonIconBuilder, defaultValue: null));
+    properties.add(DiagnosticsProperty<WidgetBuilder>('endDrawerButtonIconBuilder', endDrawerButtonIconBuilder, defaultValue: null));
+  }
+}
+
+/// An inherited widget that overrides the default icon of [BackButtonIcon],
+/// [CloseButtonIcon], [DrawerButtonIcon], and [EndDrawerButtonIcon] in this
+/// widget's subtree.
+class ActionIconTheme extends InheritedTheme {
+  /// Creates a theme that overrides the default icon of [BackButtonIcon],
+  /// [CloseButtonIcon], [DrawerButtonIcon], and [EndDrawerButtonIcon] in this
+  /// widget's subtree.
+  const ActionIconTheme({
+    super.key,
+    required this.data,
+    required super.child,
+  });
+
+  /// Specifies the default icon overrides for descendant [BackButtonIcon],
+  /// [CloseButtonIcon], [DrawerButtonIcon], and [EndDrawerButtonIcon] widgets.
+  final ActionIconThemeData data;
+
+  /// The closest instance of this class that encloses the given context.
+  ///
+  /// If there is no enclosing [ActionIconTheme] widget, then
+  /// [ThemeData.actionIconTheme] is used.
+  ///
+  /// Typical usage is as follows:
+  ///
+  /// ```dart
+  /// ActionIconThemeData? theme = ActionIconTheme.of(context);
+  /// ```
+  static ActionIconThemeData? of(BuildContext context) {
+    final ActionIconTheme? actionIconTheme = context.dependOnInheritedWidgetOfExactType<ActionIconTheme>();
+    return actionIconTheme?.data ?? Theme.of(context).actionIconTheme;
+  }
+
+  @override
+  Widget wrap(BuildContext context, Widget child) {
+    return ActionIconTheme(data: data, child: child);
+  }
+
+  @override
+  bool updateShouldNotify(ActionIconTheme oldWidget) => data != oldWidget.data;
+}
diff --git a/framework/lib/src/material/app.dart b/framework/lib/src/material/app.dart
index fb82772..50e25f5 100644
--- a/framework/lib/src/material/app.dart
+++ b/framework/lib/src/material/app.dart
@@ -910,15 +910,14 @@
     );
   }
 
-  Widget _materialBuilder(BuildContext context, Widget? child) {
+  ThemeData _themeBuilder(BuildContext context) {
+    ThemeData? theme;
     // Resolve which theme to use based on brightness and high contrast.
     final ThemeMode mode = widget.themeMode ?? ThemeMode.system;
     final Brightness platformBrightness = MediaQuery.platformBrightnessOf(context);
     final bool useDarkTheme = mode == ThemeMode.dark
       || (mode == ThemeMode.system && platformBrightness == ui.Brightness.dark);
     final bool highContrast = MediaQuery.highContrastOf(context);
-    ThemeData? theme;
-
     if (useDarkTheme && highContrast && widget.highContrastDarkTheme != null) {
       theme = widget.highContrastDarkTheme;
     } else if (useDarkTheme && widget.darkTheme != null) {
@@ -927,6 +926,11 @@
       theme = widget.highContrastTheme;
     }
     theme ??= widget.theme ?? ThemeData.light();
+    return theme;
+  }
+
+  Widget _materialBuilder(BuildContext context, Widget? child) {
+    final ThemeData theme = _themeBuilder(context);
     final Color effectiveSelectionColor = theme.textSelectionTheme.selectionColor ?? theme.colorScheme.primary.withOpacity(0.40);
     final Color effectiveCursorColor = theme.textSelectionTheme.cursorColor ?? theme.colorScheme.primary;
 
diff --git a/framework/lib/src/material/app_bar.dart b/framework/lib/src/material/app_bar.dart
index 638ed8a..8f8a9e4 100644
--- a/framework/lib/src/material/app_bar.dart
+++ b/framework/lib/src/material/app_bar.dart
@@ -9,8 +9,8 @@
 import 'package:flute/services.dart';
 import 'package:flute/widgets.dart';
 
+import 'action_buttons.dart';
 import 'app_bar_theme.dart';
-import 'back_button.dart';
 import 'button_style.dart';
 import 'color_scheme.dart';
 import 'colors.dart';
@@ -21,7 +21,6 @@
 import 'icon_button_theme.dart';
 import 'icons.dart';
 import 'material.dart';
-import 'material_localizations.dart';
 import 'material_state.dart';
 import 'scaffold.dart';
 import 'tabs.dart';
@@ -755,14 +754,6 @@
     super.dispose();
   }
 
-  void _handleDrawerButton() {
-    Scaffold.of(context).openDrawer();
-  }
-
-  void _handleDrawerButtonEnd() {
-    Scaffold.of(context).openEndDrawer();
-  }
-
   void _handleScrollNotification(ScrollNotification notification) {
     if (notification is ScrollUpdateNotification && widget.notificationPredicate(notification)) {
       final bool oldScrolledUnder = _scrolledUnder;
@@ -777,8 +768,9 @@
           break;
         case AxisDirection.right:
         case AxisDirection.left:
-          // Scrolled under is only supported in the vertical axis.
-          _scrolledUnder = false;
+          // Scrolled under is only supported in the vertical axis, and should
+          // not be altered based on horizontal notifications of the same
+          // predicate since it could be a 2D scroller.
           break;
       }
 
@@ -894,11 +886,8 @@
     Widget? leading = widget.leading;
     if (leading == null && widget.automaticallyImplyLeading) {
       if (hasDrawer) {
-        leading = IconButton(
-          icon: const Icon(Icons.menu),
-          iconSize: overallIconTheme.size ?? 24,
-          onPressed: _handleDrawerButton,
-          tooltip: MaterialLocalizations.of(context).openAppDrawerTooltip,
+        leading = DrawerButton(
+          style: IconButton.styleFrom(iconSize: overallIconTheme.size ?? 24),
         );
         // TODO(chunhtai): remove (!hasEndDrawer && canPop) once internal tests
         // are migrated.
@@ -1009,11 +998,8 @@
         children: widget.actions!,
       );
     } else if (hasEndDrawer) {
-      actions = IconButton(
-        icon: const Icon(Icons.menu),
-        iconSize: overallIconTheme.size ?? 24,
-        onPressed: _handleDrawerButtonEnd,
-        tooltip: MaterialLocalizations.of(context).openAppDrawerTooltip,
+      actions = EndDrawerButton(
+        style: IconButton.styleFrom(iconSize: overallIconTheme.size ?? 24),
       );
     }
 
@@ -1543,13 +1529,16 @@
       key: key,
       leading: leading,
       automaticallyImplyLeading: automaticallyImplyLeading,
-      actions: actions,
       flexibleSpace: flexibleSpace ?? _ScrollUnderFlexibleSpace(
+        hasLeading: leading != null,
         title: title,
+        actions: actions,
         foregroundColor: foregroundColor,
         variant: _ScrollUnderFlexibleVariant.medium,
         centerCollapsedTitle: centerTitle,
         primary: primary,
+        leadingWidth: leadingWidth,
+        titleSpacing: titleSpacing,
       ),
       bottom: bottom,
       elevation: elevation,
@@ -1645,13 +1634,16 @@
       key: key,
       leading: leading,
       automaticallyImplyLeading: automaticallyImplyLeading,
-      actions: actions,
       flexibleSpace: flexibleSpace ?? _ScrollUnderFlexibleSpace(
+        hasLeading: leading != null,
         title: title,
+        actions: actions,
         foregroundColor: foregroundColor,
         variant: _ScrollUnderFlexibleVariant.large,
         centerCollapsedTitle: centerTitle,
         primary: primary,
+        leadingWidth: leadingWidth,
+        titleSpacing: titleSpacing,
       ),
       bottom: bottom,
       elevation: elevation,
@@ -2092,18 +2084,26 @@
 
 class _ScrollUnderFlexibleSpace extends StatelessWidget {
   const _ScrollUnderFlexibleSpace({
+    required this.hasLeading,
     this.title,
+    this.actions,
     this.foregroundColor,
     required this.variant,
     this.centerCollapsedTitle,
     this.primary = true,
+    this.leadingWidth,
+    this.titleSpacing,
   });
 
+  final bool hasLeading;
   final Widget? title;
+  final List<Widget>? actions;
   final Color? foregroundColor;
   final _ScrollUnderFlexibleVariant variant;
   final bool? centerCollapsedTitle;
   final bool primary;
+  final double? leadingWidth;
+  final double? titleSpacing;
 
   @override
   Widget build(BuildContext context) {
@@ -2157,6 +2157,14 @@
       centerTitle = centerCollapsedTitle ?? appBarTheme.centerTitle ?? platformCenter();
     }
 
+    EdgeInsetsGeometry effectiveCollapsedTitlePadding = EdgeInsets.zero;
+    if (hasLeading && leadingWidth == null) {
+      effectiveCollapsedTitlePadding = centerTitle
+        ? config.collapsedCenteredTitlePadding!
+        : config.collapsedTitlePadding!;
+    } else if (hasLeading && leadingWidth != null) {
+      effectiveCollapsedTitlePadding = EdgeInsetsDirectional.only(start: leadingWidth!);
+    }
     final bool isCollapsed = settings.isScrolledUnder ?? false;
     return Column(
       children: <Widget>[
@@ -2164,17 +2172,20 @@
           padding: EdgeInsets.only(top: topPadding),
           child: Container(
             height: collapsedHeight,
-            padding: centerTitle ? config.collapsedCenteredTitlePadding : config.collapsedTitlePadding,
-            child: AnimatedOpacity(
-              opacity: isCollapsed ? 1 : 0,
-              duration: const Duration(milliseconds: 500),
-              curve: const Cubic(0.2, 0.0, 0.0, 1.0),
-              child: Align(
-                alignment: centerTitle
-                  ? Alignment.center
-                  : AlignmentDirectional.centerStart,
+            padding: effectiveCollapsedTitlePadding,
+            child: NavigationToolbar(
+              centerMiddle: centerTitle,
+              middleSpacing: titleSpacing ?? appBarTheme.titleSpacing ?? NavigationToolbar.kMiddleSpacing,
+              middle: AnimatedOpacity(
+                opacity: isCollapsed ? 1 : 0,
+                duration: const Duration(milliseconds: 500),
+                curve: const Cubic(0.2, 0.0, 0.0, 1.0),
                 child: collapsedTitle,
               ),
+              trailing: actions != null ? Row(
+                mainAxisSize: MainAxisSize.min,
+                children: actions!,
+              ) : null,
             ),
           ),
         ),
@@ -2310,10 +2321,10 @@
     _textTheme.headlineSmall?.apply(color: _colors.onSurface);
 
   @override
-  EdgeInsetsGeometry? get collapsedTitlePadding => const EdgeInsetsDirectional.fromSTEB(48, 0, 16, 0);
+  EdgeInsetsGeometry? get collapsedTitlePadding => const EdgeInsetsDirectional.only(start: 40);
 
   @override
-  EdgeInsetsGeometry? get collapsedCenteredTitlePadding => const EdgeInsets.fromLTRB(16, 0, 16, 0);
+  EdgeInsetsGeometry? get collapsedCenteredTitlePadding => const EdgeInsetsDirectional.only(start: 40);
 
   @override
   EdgeInsetsGeometry? get expandedTitlePadding => const EdgeInsets.fromLTRB(16, 0, 16, 20);
@@ -2339,10 +2350,10 @@
     _textTheme.headlineMedium?.apply(color: _colors.onSurface);
 
   @override
-  EdgeInsetsGeometry? get collapsedTitlePadding => const EdgeInsetsDirectional.fromSTEB(48, 0, 16, 0);
+  EdgeInsetsGeometry? get collapsedTitlePadding => const EdgeInsetsDirectional.only(start: 40);
 
   @override
-  EdgeInsetsGeometry? get collapsedCenteredTitlePadding => const EdgeInsets.fromLTRB(16, 0, 16, 0);
+  EdgeInsetsGeometry? get collapsedCenteredTitlePadding => const EdgeInsetsDirectional.only(start: 40);
 
   @override
   EdgeInsetsGeometry? get expandedTitlePadding => const EdgeInsets.fromLTRB(16, 0, 16, 28);
diff --git a/framework/lib/src/material/app_bar_theme.dart b/framework/lib/src/material/app_bar_theme.dart
index dae83c0..7f4952f 100644
--- a/framework/lib/src/material/app_bar_theme.dart
+++ b/framework/lib/src/material/app_bar_theme.dart
@@ -204,6 +204,9 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static AppBarTheme lerp(AppBarTheme? a, AppBarTheme? b, double t) {
+    if (identical(a, b) && a != null) {
+      return a;
+    }
     return AppBarTheme(
       backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t),
       foregroundColor: Color.lerp(a?.foregroundColor, b?.foregroundColor, t),
diff --git a/framework/lib/src/material/back_button.dart b/framework/lib/src/material/back_button.dart
index 3e88494..95f4b97 100644
--- a/framework/lib/src/material/back_button.dart
+++ b/framework/lib/src/material/back_button.dart
@@ -2,197 +2,4 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import 'package:flute/foundation.dart';
-import 'package:flute/widgets.dart';
-
-import 'debug.dart';
-import 'icon_button.dart';
-import 'icons.dart';
-import 'material_localizations.dart';
-import 'theme.dart';
-
-/// A "back" icon that's appropriate for the current [TargetPlatform].
-///
-/// The current platform is determined by querying for the ambient [Theme].
-///
-/// See also:
-///
-///  * [BackButton], an [IconButton] with a [BackButtonIcon] that calls
-///    [Navigator.maybePop] to return to the previous route.
-///  * [IconButton], which is a more general widget for creating buttons
-///    with icons.
-///  * [Icon], a Material Design icon.
-///  * [ThemeData.platform], which specifies the current platform.
-class BackButtonIcon extends StatelessWidget {
-  /// Creates an icon that shows the appropriate "back" image for
-  /// the current platform (as obtained from the [Theme]).
-  const BackButtonIcon({ super.key });
-
-  @override
-  Widget build(BuildContext context) {
-    final String? semanticsLabel;
-    final IconData data;
-    switch (Theme.of(context).platform) {
-      case TargetPlatform.android:
-      case TargetPlatform.fuchsia:
-      case TargetPlatform.linux:
-      case TargetPlatform.windows:
-        data = Icons.arrow_back;
-        break;
-      case TargetPlatform.iOS:
-      case TargetPlatform.macOS:
-        data = Icons.arrow_back_ios;
-        break;
-    }
-    // This can't use the platform from Theme because it is the Android OS that
-    // expects the duplicated tooltip and label.
-    switch (defaultTargetPlatform) {
-      case TargetPlatform.android:
-        semanticsLabel = MaterialLocalizations.of(context).backButtonTooltip;
-        break;
-      case TargetPlatform.fuchsia:
-      case TargetPlatform.linux:
-      case TargetPlatform.windows:
-      case TargetPlatform.iOS:
-      case TargetPlatform.macOS:
-        semanticsLabel = null;
-        break;
-    }
-
-    return Icon(data, semanticLabel: semanticsLabel);
-  }
-}
-
-/// A Material Design back button.
-///
-/// A [BackButton] is an [IconButton] with a "back" icon appropriate for the
-/// current [TargetPlatform]. When pressed, the back button calls
-/// [Navigator.maybePop] to return to the previous route unless a custom
-/// [onPressed] callback is provided.
-///
-/// When deciding to display a [BackButton], consider using
-/// `ModalRoute.of(context)?.canPop` to check whether the current route can be
-/// popped. If that value is false (e.g., because the current route is the
-/// initial route), the [BackButton] will not have any effect when pressed,
-/// which could frustrate the user.
-///
-/// Requires one of its ancestors to be a [Material] widget.
-///
-/// See also:
-///
-///  * [AppBar], which automatically uses a [BackButton] in its
-///    [AppBar.leading] slot when the [Scaffold] has no [Drawer] and the
-///    current [Route] is not the [Navigator]'s first route.
-///  * [BackButtonIcon], which is useful if you need to create a back button
-///    that responds differently to being pressed.
-///  * [IconButton], which is a more general widget for creating buttons with
-///    icons.
-///  * [CloseButton], an alternative which may be more appropriate for leaf
-///    node pages in the navigation tree.
-class BackButton extends StatelessWidget {
-  /// Creates an [IconButton] with the appropriate "back" icon for the current
-  /// target platform.
-  const BackButton({ super.key, this.color, this.onPressed });
-
-  /// The color to use for the icon.
-  ///
-  /// Defaults to the [IconThemeData.color] specified in the ambient [IconTheme],
-  /// which usually matches the ambient [Theme]'s [ThemeData.iconTheme].
-  final Color? color;
-
-  /// An override callback to perform instead of the default behavior which is
-  /// to pop the [Navigator].
-  ///
-  /// It can, for instance, be used to pop the platform's navigation stack
-  /// via [SystemNavigator] instead of Flutter's [Navigator] in add-to-app
-  /// situations.
-  ///
-  /// Defaults to null.
-  final VoidCallback? onPressed;
-
-  @override
-  Widget build(BuildContext context) {
-    assert(debugCheckHasMaterialLocalizations(context));
-    return IconButton(
-      icon: const BackButtonIcon(),
-      color: color,
-      tooltip: MaterialLocalizations.of(context).backButtonTooltip,
-      onPressed: () {
-        if (onPressed != null) {
-          onPressed!();
-        } else {
-          Navigator.maybePop(context);
-        }
-      },
-    );
-  }
-}
-
-/// A Material Design close button.
-///
-/// A [CloseButton] is an [IconButton] with a "close" icon. When pressed, the
-/// close button calls [Navigator.maybePop] to return to the previous route.
-///
-/// Use a [CloseButton] instead of a [BackButton] on fullscreen dialogs or
-/// pages that may solicit additional actions to close.
-///
-/// See also:
-///
-///  * [AppBar], which automatically uses a [CloseButton] in its
-///    [AppBar.leading] slot when appropriate.
-///  * [BackButton], which is more appropriate for middle nodes in the
-///    navigation tree or where pages can be popped instantaneously with
-///    no user data consequence.
-///  * [IconButton], to create other Material Design icon buttons.
-class CloseButton extends StatelessWidget {
-  /// Creates a Material Design close button.
-  const CloseButton({ super.key, this.color, this.onPressed });
-
-  /// The color to use for the icon.
-  ///
-  /// Defaults to the [IconThemeData.color] specified in the ambient [IconTheme],
-  /// which usually matches the ambient [Theme]'s [ThemeData.iconTheme].
-  final Color? color;
-
-  /// An override callback to perform instead of the default behavior which is
-  /// to pop the [Navigator].
-  ///
-  /// It can, for instance, be used to pop the platform's navigation stack
-  /// via [SystemNavigator] instead of Flutter's [Navigator] in add-to-app
-  /// situations.
-  ///
-  /// Defaults to null.
-  final VoidCallback? onPressed;
-
-  @override
-  Widget build(BuildContext context) {
-    assert(debugCheckHasMaterialLocalizations(context));
-    final String? semanticsLabel;
-    // This can't use the platform from Theme because it is the Android OS that
-    // expects the duplicated tooltip and label.
-    switch (defaultTargetPlatform) {
-      case TargetPlatform.android:
-        semanticsLabel = MaterialLocalizations.of(context).closeButtonTooltip;
-        break;
-      case TargetPlatform.fuchsia:
-      case TargetPlatform.linux:
-      case TargetPlatform.windows:
-      case TargetPlatform.iOS:
-      case TargetPlatform.macOS:
-        semanticsLabel = null;
-        break;
-    }
-    return IconButton(
-      icon: Icon(Icons.close, semanticLabel: semanticsLabel),
-      color: color,
-      tooltip: MaterialLocalizations.of(context).closeButtonTooltip,
-      onPressed: () {
-        if (onPressed != null) {
-          onPressed!();
-        } else {
-          Navigator.maybePop(context);
-        }
-      },
-    );
-  }
-}
+export 'action_buttons.dart' show BackButton, BackButtonIcon, CloseButton, CloseButtonIcon;
diff --git a/framework/lib/src/material/badge_theme.dart b/framework/lib/src/material/badge_theme.dart
index 1095100..7666bd4 100644
--- a/framework/lib/src/material/badge_theme.dart
+++ b/framework/lib/src/material/badge_theme.dart
@@ -94,6 +94,9 @@
 
   /// Linearly interpolate between two [Badge] themes.
   static BadgeThemeData lerp(BadgeThemeData? a, BadgeThemeData? b, double t) {
+    if (identical(a, b) && a != null) {
+      return a;
+    }
     return BadgeThemeData(
       backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t),
       textColor: Color.lerp(a?.textColor, b?.textColor, t),
diff --git a/framework/lib/src/material/bottom_app_bar.dart b/framework/lib/src/material/bottom_app_bar.dart
index 41e83cb..ed91989 100644
--- a/framework/lib/src/material/bottom_app_bar.dart
+++ b/framework/lib/src/material/bottom_app_bar.dart
@@ -6,6 +6,7 @@
 import 'package:flute/widgets.dart';
 
 import 'bottom_app_bar_theme.dart';
+import 'colors.dart';
 import 'elevation_overlay.dart';
 import 'material.dart';
 import 'scaffold.dart';
@@ -71,6 +72,7 @@
     this.child,
     this.padding,
     this.surfaceTintColor,
+    this.shadowColor,
     this.height,
   }) : assert(elevation == null || elevation >= 0.0);
 
@@ -135,6 +137,18 @@
   /// See [Material.surfaceTintColor] for more details on how this overlay is applied.
   final Color? surfaceTintColor;
 
+  /// The color of the shadow below the app bar.
+  ///
+  /// If this property is null, then [BottomAppBarTheme.shadowColor] of
+  /// [ThemeData.bottomAppBarTheme] is used. If that is also null, the default value
+  /// is fully opaque black for Material 2, and transparent for Material 3.
+  ///
+  /// See also:
+  ///
+  ///  * [elevation], which defines the size of the shadow below the app bar.
+  ///  * [shape], which defines the shape of the app bar and its shadow.
+  final Color? shadowColor;
+
   /// The double value used to indicate the height of the [BottomAppBar].
   ///
   /// If this is null, the default value is the minimum in relation to the content,
@@ -177,29 +191,33 @@
     final Color color = widget.color ?? babTheme.color ?? defaults.color!;
     final Color surfaceTintColor = widget.surfaceTintColor ?? babTheme.surfaceTintColor ?? defaults.surfaceTintColor!;
     final Color effectiveColor = isMaterial3 ? color : ElevationOverlay.applyOverlay(context, color, elevation);
+    final Color shadowColor = widget.shadowColor ?? babTheme.shadowColor ?? defaults.shadowColor!;
 
     final Widget child = Padding(
       padding: widget.padding ?? babTheme.padding ?? (isMaterial3 ? const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0) : EdgeInsets.zero),
       child: widget.child,
     );
 
-    return SizedBox(
-      height: height,
-      child: PhysicalShape(
-        clipper: clipper,
-        elevation: elevation,
-        color: effectiveColor,
-        clipBehavior: widget.clipBehavior,
-        child: Material(
-          key: materialKey,
-          type: isMaterial3 ? MaterialType.canvas : MaterialType.transparency,
-          elevation: elevation,
-          color: isMaterial3 ? effectiveColor : null,
-          surfaceTintColor: surfaceTintColor,
-          child: SafeArea(child: child),
-        ),
-      ),
+    final Material material = Material(
+      key: materialKey,
+      type: isMaterial3 ? MaterialType.canvas : MaterialType.transparency,
+      elevation: elevation,
+      color: isMaterial3 ? effectiveColor : null,
+      surfaceTintColor: surfaceTintColor,
+      shadowColor: shadowColor,
+      child: SafeArea(child: child),
     );
+
+    final PhysicalShape physicalShape = PhysicalShape(
+      clipper: clipper,
+      elevation: elevation,
+      shadowColor: shadowColor,
+      color: effectiveColor,
+      clipBehavior: widget.clipBehavior,
+      child: material,
+     );
+
+    return SizedBox(height: height, child: physicalShape);
   }
 }
 
@@ -260,6 +278,9 @@
 
   @override
   Color? get surfaceTintColor => Theme.of(context).colorScheme.surfaceTint;
+
+  @override
+  Color get shadowColor => const Color(0xFF000000);
 }
 
 // BEGIN GENERATED TOKEN PROPERTIES - BottomAppBar
@@ -286,6 +307,9 @@
 
   @override
   Color? get surfaceTintColor => Theme.of(context).colorScheme.surfaceTint;
+
+  @override
+  Color get shadowColor => Colors.transparent;
 }
 
 // END GENERATED TOKEN PROPERTIES - BottomAppBar
diff --git a/framework/lib/src/material/bottom_app_bar_theme.dart b/framework/lib/src/material/bottom_app_bar_theme.dart
index e59c538..86f4a25 100644
--- a/framework/lib/src/material/bottom_app_bar_theme.dart
+++ b/framework/lib/src/material/bottom_app_bar_theme.dart
@@ -34,6 +34,7 @@
     this.shape,
     this.height,
     this.surfaceTintColor,
+    this.shadowColor,
     this.padding,
   });
 
@@ -49,8 +50,6 @@
   final NotchedShape? shape;
 
   /// Overrides the default value for [BottomAppBar.height].
-  ///
-  /// If null, [BottomAppBar] height will be the minimum on the non material 3.
   final double? height;
 
   /// Overrides the default value for [BottomAppBar.surfaceTintColor].
@@ -60,6 +59,9 @@
   /// See [Material.surfaceTintColor] for more details.
   final Color? surfaceTintColor;
 
+  /// Overrides the default value for [BottomAppBar.shadowColor].
+  final Color? shadowColor;
+
   /// Overrides the default value for [BottomAppBar.padding].
   final EdgeInsetsGeometry? padding;
 
@@ -71,6 +73,7 @@
     NotchedShape? shape,
     double? height,
     Color? surfaceTintColor,
+    Color? shadowColor,
     EdgeInsetsGeometry? padding,
   }) {
     return BottomAppBarTheme(
@@ -79,6 +82,7 @@
       shape: shape ?? this.shape,
       height: height ?? this.height,
       surfaceTintColor: surfaceTintColor ?? this.surfaceTintColor,
+      shadowColor: shadowColor ?? this.shadowColor,
       padding: padding ?? this.padding,
     );
   }
@@ -94,12 +98,16 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static BottomAppBarTheme lerp(BottomAppBarTheme? a, BottomAppBarTheme? b, double t) {
+    if (identical(a, b) && a != null) {
+      return a;
+    }
     return BottomAppBarTheme(
       color: Color.lerp(a?.color, b?.color, t),
       elevation: lerpDouble(a?.elevation, b?.elevation, t),
       shape: t < 0.5 ? a?.shape : b?.shape,
       height: lerpDouble(a?.height, b?.height, t),
       surfaceTintColor: Color.lerp(a?.surfaceTintColor, b?.surfaceTintColor, t),
+      shadowColor: Color.lerp(a?.shadowColor, b?.shadowColor, t),
       padding: EdgeInsetsGeometry.lerp(a?.padding, b?.padding, t),
     );
   }
@@ -111,6 +119,7 @@
     shape,
     height,
     surfaceTintColor,
+    shadowColor,
     padding,
   );
 
@@ -128,6 +137,7 @@
         && other.shape == shape
         && other.height == height
         && other.surfaceTintColor == surfaceTintColor
+        && other.shadowColor == shadowColor
         && other.padding == padding;
   }
 
@@ -139,6 +149,7 @@
     properties.add(DiagnosticsProperty<NotchedShape>('shape', shape, defaultValue: null));
     properties.add(DiagnosticsProperty<double>('height', height, defaultValue: null));
     properties.add(ColorProperty('surfaceTintColor', surfaceTintColor, defaultValue: null));
+    properties.add(ColorProperty('shadowColor', shadowColor, defaultValue: null));
     properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding, defaultValue: null));
   }
 }
diff --git a/framework/lib/src/material/bottom_navigation_bar_theme.dart b/framework/lib/src/material/bottom_navigation_bar_theme.dart
index 7d150ef..ed515a5 100644
--- a/framework/lib/src/material/bottom_navigation_bar_theme.dart
+++ b/framework/lib/src/material/bottom_navigation_bar_theme.dart
@@ -174,6 +174,9 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static BottomNavigationBarThemeData lerp(BottomNavigationBarThemeData? a, BottomNavigationBarThemeData? b, double t) {
+    if (identical(a, b) && a != null) {
+      return a;
+    }
     return BottomNavigationBarThemeData(
       backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t),
       elevation: lerpDouble(a?.elevation, b?.elevation, t),
diff --git a/framework/lib/src/material/bottom_sheet.dart b/framework/lib/src/material/bottom_sheet.dart
index 887d7e2..dfb37f8 100644
--- a/framework/lib/src/material/bottom_sheet.dart
+++ b/framework/lib/src/material/bottom_sheet.dart
@@ -78,6 +78,7 @@
     this.onDragStart,
     this.onDragEnd,
     this.backgroundColor,
+    this.shadowColor,
     this.elevation,
     this.shape,
     this.clipBehavior,
@@ -134,6 +135,18 @@
   /// Defaults to null and falls back to [Material]'s default.
   final Color? backgroundColor;
 
+  /// The color of the shadow below the sheet.
+  ///
+  /// If this property is null, then [BottomSheetThemeData.shadowColor] of
+  /// [ThemeData.bottomSheetTheme] is used. If that is also null, the default value
+  /// is transparent.
+  ///
+  /// See also:
+  ///
+  ///  * [elevation], which defines the size of the shadow below the sheet.
+  ///  * [shape], which defines the shape of the sheet and its shadow.
+  final Color? shadowColor;
+
   /// The z-coordinate at which to place this material relative to its parent.
   ///
   /// This controls the size of the shadow below the material.
@@ -275,6 +288,7 @@
     final BoxConstraints? constraints = widget.constraints ?? bottomSheetTheme.constraints;
     final Color? color = widget.backgroundColor ?? bottomSheetTheme.backgroundColor ?? defaults.backgroundColor;
     final Color? surfaceTintColor = bottomSheetTheme.surfaceTintColor ?? defaults.surfaceTintColor;
+    final Color? shadowColor = widget.shadowColor ?? bottomSheetTheme.shadowColor ?? defaults.shadowColor;
     final double elevation = widget.elevation ?? bottomSheetTheme.elevation ?? defaults.elevation ?? 0;
     final ShapeBorder? shape = widget.shape ?? bottomSheetTheme.shape ?? defaults.shape;
     final Clip clipBehavior = widget.clipBehavior ?? bottomSheetTheme.clipBehavior ?? Clip.none;
@@ -284,6 +298,7 @@
       color: color,
       elevation: elevation,
       surfaceTintColor: surfaceTintColor,
+      shadowColor: shadowColor,
       shape: shape,
       clipBehavior: clipBehavior,
       child: NotificationListener<DraggableScrollableNotification>(
@@ -559,10 +574,8 @@
         onDragEnd: handleDragEnd,
       ),
       builder: (BuildContext context, Widget? child) {
-        // Disable the initial animation when accessible navigation is on so
-        // that the semantics are added to the tree at the correct time.
         final double animationValue = animationCurve.transform(
-            MediaQuery.accessibleNavigationOf(context) ? 1.0 : widget.route.animation!.value,
+            widget.route.animation!.value,
         );
         return Semantics(
           scopesRoute: true,
@@ -1144,6 +1157,9 @@
 
   @override
   Color? get surfaceTintColor => Theme.of(context).colorScheme.surfaceTint;
+
+  @override
+  Color? get shadowColor => Colors.transparent;
 }
 
 // END GENERATED TOKEN PROPERTIES - BottomSheet
diff --git a/framework/lib/src/material/bottom_sheet_theme.dart b/framework/lib/src/material/bottom_sheet_theme.dart
index c0ebc22..079786a 100644
--- a/framework/lib/src/material/bottom_sheet_theme.dart
+++ b/framework/lib/src/material/bottom_sheet_theme.dart
@@ -33,6 +33,7 @@
     this.elevation,
     this.modalBackgroundColor,
     this.modalBarrierColor,
+    this.shadowColor,
     this.modalElevation,
     this.shape,
     this.clipBehavior,
@@ -66,6 +67,9 @@
   /// a modal bottom sheet.
   final Color? modalBarrierColor;
 
+  /// Overrides the default value for [BottomSheet.shadowColor].
+  final Color? shadowColor;
+
   /// Value for [BottomSheet.elevation] when the Bottom sheet is presented as a
   /// modal bottom sheet.
   final double? modalElevation;
@@ -94,6 +98,7 @@
     double? elevation,
     Color? modalBackgroundColor,
     Color? modalBarrierColor,
+    Color? shadowColor,
     double? modalElevation,
     ShapeBorder? shape,
     Clip? clipBehavior,
@@ -105,6 +110,7 @@
       elevation: elevation ?? this.elevation,
       modalBackgroundColor: modalBackgroundColor ?? this.modalBackgroundColor,
       modalBarrierColor: modalBarrierColor ?? this.modalBarrierColor,
+      shadowColor: shadowColor ?? this.shadowColor,
       modalElevation: modalElevation ?? this.modalElevation,
       shape: shape ?? this.shape,
       clipBehavior: clipBehavior ?? this.clipBehavior,
@@ -118,8 +124,8 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static BottomSheetThemeData? lerp(BottomSheetThemeData? a, BottomSheetThemeData? b, double t) {
-    if (a == null && b == null) {
-      return null;
+    if (identical(a, b)) {
+      return a;
     }
     return BottomSheetThemeData(
       backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t),
@@ -127,6 +133,7 @@
       elevation: lerpDouble(a?.elevation, b?.elevation, t),
       modalBackgroundColor: Color.lerp(a?.modalBackgroundColor, b?.modalBackgroundColor, t),
       modalBarrierColor: Color.lerp(a?.modalBarrierColor, b?.modalBarrierColor, t),
+      shadowColor: Color.lerp(a?.shadowColor, b?.shadowColor, t),
       modalElevation: lerpDouble(a?.modalElevation, b?.modalElevation, t),
       shape: ShapeBorder.lerp(a?.shape, b?.shape, t),
       clipBehavior: t < 0.5 ? a?.clipBehavior : b?.clipBehavior,
@@ -141,6 +148,7 @@
     elevation,
     modalBackgroundColor,
     modalBarrierColor,
+    shadowColor,
     modalElevation,
     shape,
     clipBehavior,
@@ -160,6 +168,7 @@
         && other.surfaceTintColor == surfaceTintColor
         && other.elevation == elevation
         && other.modalBackgroundColor == modalBackgroundColor
+        && other.shadowColor == shadowColor
         && other.modalBarrierColor == modalBarrierColor
         && other.modalElevation == modalElevation
         && other.shape == shape
@@ -174,6 +183,7 @@
     properties.add(ColorProperty('surfaceTintColor', surfaceTintColor, defaultValue: null));
     properties.add(DoubleProperty('elevation', elevation, defaultValue: null));
     properties.add(ColorProperty('modalBackgroundColor', modalBackgroundColor, defaultValue: null));
+    properties.add(ColorProperty('shadowColor', shadowColor, defaultValue: null));
     properties.add(ColorProperty('modalBarrierColor', modalBarrierColor, defaultValue: null));
     properties.add(DoubleProperty('modalElevation', modalElevation, defaultValue: null));
     properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null));
diff --git a/framework/lib/src/material/button_bar.dart b/framework/lib/src/material/button_bar.dart
index 069c285..d87ab4f 100644
--- a/framework/lib/src/material/button_bar.dart
+++ b/framework/lib/src/material/button_bar.dart
@@ -146,15 +146,14 @@
 
   /// The spacing between buttons when the button bar overflows.
   ///
-  /// If the [children] do not fit into a single row, they are
-  /// arranged into a column. This parameter provides additional
-  /// vertical space in between buttons when it does overflow.
+  /// If the [children] do not fit into a single row, they are arranged into a
+  /// column. This parameter provides additional vertical space in between
+  /// buttons when it does overflow.
   ///
-  /// Note that the button spacing may appear to be more than
-  /// the value provided. This is because most buttons adhere to the
-  /// [MaterialTapTargetSize] of 48px. So, even though a button
-  /// might visually be 36px in height, it might still take up to
-  /// 48px vertically.
+  /// The button spacing may appear to be more than the value provided. This is
+  /// because most buttons adhere to the [MaterialTapTargetSize] of 48px. So,
+  /// even though a button might visually be 36px in height, it might still take
+  /// up to 48px vertically.
   ///
   /// If null then no spacing will be added in between buttons in
   /// an overflow state.
diff --git a/framework/lib/src/material/button_bar_theme.dart b/framework/lib/src/material/button_bar_theme.dart
index 20df2ae..d9c54bd 100644
--- a/framework/lib/src/material/button_bar_theme.dart
+++ b/framework/lib/src/material/button_bar_theme.dart
@@ -146,8 +146,8 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static ButtonBarThemeData? lerp(ButtonBarThemeData? a, ButtonBarThemeData? b, double t) {
-    if (a == null && b == null) {
-      return null;
+    if (identical(a, b)) {
+      return a;
     }
     return ButtonBarThemeData(
       alignment: t < 0.5 ? a?.alignment : b?.alignment,
diff --git a/framework/lib/src/material/button_style.dart b/framework/lib/src/material/button_style.dart
index 18cf679..440cac0 100644
--- a/framework/lib/src/material/button_style.dart
+++ b/framework/lib/src/material/button_style.dart
@@ -492,8 +492,8 @@
 
   /// Linearly interpolate between two [ButtonStyle]s.
   static ButtonStyle? lerp(ButtonStyle? a, ButtonStyle? b, double t) {
-    if (a == null && b == null) {
-      return null;
+    if (identical(a, b)) {
+      return a;
     }
     return ButtonStyle(
       textStyle: MaterialStateProperty.lerp<TextStyle?>(a?.textStyle, b?.textStyle, t, TextStyle.lerp),
diff --git a/framework/lib/src/material/button_theme.dart b/framework/lib/src/material/button_theme.dart
index 7e38bf1..62157d8 100644
--- a/framework/lib/src/material/button_theme.dart
+++ b/framework/lib/src/material/button_theme.dart
@@ -23,7 +23,7 @@
   /// Button text is black or white depending on [ThemeData.brightness].
   normal,
 
-  /// Button text is [ThemeData.accentColor].
+  /// Button text is [ColorScheme.secondary].
   accent,
 
   /// Button text is based on [ThemeData.primaryColor].
diff --git a/framework/lib/src/material/card_theme.dart b/framework/lib/src/material/card_theme.dart
index db849ee..b4376b7 100644
--- a/framework/lib/src/material/card_theme.dart
+++ b/framework/lib/src/material/card_theme.dart
@@ -114,6 +114,9 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static CardTheme lerp(CardTheme? a, CardTheme? b, double t) {
+    if (identical(a, b) && a != null) {
+      return a;
+    }
     return CardTheme(
       clipBehavior: t < 0.5 ? a?.clipBehavior : b?.clipBehavior,
       color: Color.lerp(a?.color, b?.color, t),
diff --git a/framework/lib/src/material/checkbox.dart b/framework/lib/src/material/checkbox.dart
index 39b8846..1b88890 100644
--- a/framework/lib/src/material/checkbox.dart
+++ b/framework/lib/src/material/checkbox.dart
@@ -305,7 +305,7 @@
   ///
   /// If this property is null then [CheckboxThemeData.shape] of [ThemeData.checkboxTheme]
   /// is used. If that's null then the shape will be a [RoundedRectangleBorder]
-  /// with a circular corner radius of 1.0.
+  /// with a circular corner radius of 1.0 in Material 2, and 2.0 in Material 3.
   final OutlinedBorder? shape;
 
   /// {@template flutter.material.checkbox.side}
@@ -522,9 +522,7 @@
           ..checkColor = effectiveCheckColor
           ..value = value
           ..previousValue = _previousValue
-          ..shape = widget.shape ?? checkboxTheme.shape ?? const RoundedRectangleBorder(
-              borderRadius: BorderRadius.all(Radius.circular(1.0)),
-          )
+          ..shape = widget.shape ?? checkboxTheme.shape ?? defaults.shape!
           ..side = _resolveSide(widget.side) ?? _resolveSide(checkboxTheme.side),
       ),
     );
@@ -759,6 +757,11 @@
 
   @override
   VisualDensity get visualDensity => _theme.visualDensity;
+
+  @override
+  OutlinedBorder get shape => const RoundedRectangleBorder(
+    borderRadius: BorderRadius.all(Radius.circular(1.0)),
+  );
 }
 
 // BEGIN GENERATED TOKEN PROPERTIES - Checkbox
@@ -869,6 +872,11 @@
 
   @override
   VisualDensity get visualDensity => _theme.visualDensity;
+
+  @override
+  OutlinedBorder get shape => const RoundedRectangleBorder(
+    borderRadius: BorderRadius.all(Radius.circular(2.0)),
+  );
 }
 
 // END GENERATED TOKEN PROPERTIES - Checkbox
diff --git a/framework/lib/src/material/checkbox_theme.dart b/framework/lib/src/material/checkbox_theme.dart
index 7d99ecd..ceb01bb 100644
--- a/framework/lib/src/material/checkbox_theme.dart
+++ b/framework/lib/src/material/checkbox_theme.dart
@@ -130,6 +130,9 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static CheckboxThemeData lerp(CheckboxThemeData? a, CheckboxThemeData? b, double t) {
+    if (identical(a, b) && a != null) {
+      return a;
+    }
     return CheckboxThemeData(
       mouseCursor: t < 0.5 ? a?.mouseCursor : b?.mouseCursor,
       fillColor: MaterialStateProperty.lerp<Color?>(a?.fillColor, b?.fillColor, t, Color.lerp),
@@ -195,6 +198,9 @@
     if (a == null || b == null) {
       return null;
     }
+    if (identical(a, b)) {
+      return a;
+    }
     return BorderSide.lerp(a, b, t);
   }
 }
diff --git a/framework/lib/src/material/chip_theme.dart b/framework/lib/src/material/chip_theme.dart
index 63e65bc..661411c 100644
--- a/framework/lib/src/material/chip_theme.dart
+++ b/framework/lib/src/material/chip_theme.dart
@@ -483,8 +483,8 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static ChipThemeData? lerp(ChipThemeData? a, ChipThemeData? b, double t) {
-    if (a == null && b == null) {
-      return null;
+    if (identical(a, b)) {
+      return a;
     }
     return ChipThemeData(
       backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t),
diff --git a/framework/lib/src/material/color_scheme.dart b/framework/lib/src/material/color_scheme.dart
index 2e55a26..0c209f5 100644
--- a/framework/lib/src/material/color_scheme.dart
+++ b/framework/lib/src/material/color_scheme.dart
@@ -827,6 +827,9 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static ColorScheme lerp(ColorScheme a, ColorScheme b, double t) {
+    if (identical(a, b)) {
+      return a;
+    }
     return ColorScheme(
       brightness: t < 0.5 ? a.brightness : b.brightness,
       primary: Color.lerp(a.primary, b.primary, t)!,
diff --git a/framework/lib/src/material/data_table.dart b/framework/lib/src/material/data_table.dart
index ba4f434..2f49028 100644
--- a/framework/lib/src/material/data_table.dart
+++ b/framework/lib/src/material/data_table.dart
@@ -392,7 +392,13 @@
     this.onSelectAll,
     this.decoration,
     this.dataRowColor,
-    this.dataRowHeight,
+    @Deprecated(
+      'Migrate to use dataRowMinHeight and dataRowMaxHeight instead. '
+      'This feature was deprecated after v3.7.0-5.0.pre.',
+    )
+    double? dataRowHeight,
+    double? dataRowMinHeight,
+    double? dataRowMaxHeight,
     this.dataTextStyle,
     this.headingRowColor,
     this.headingRowHeight,
@@ -410,6 +416,11 @@
        assert(sortColumnIndex == null || (sortColumnIndex >= 0 && sortColumnIndex < columns.length)),
        assert(!rows.any((DataRow row) => row.cells.length != columns.length)),
        assert(dividerThickness == null || dividerThickness >= 0),
+       assert(dataRowMinHeight == null || dataRowMaxHeight == null || dataRowMaxHeight >= dataRowMinHeight),
+       assert(dataRowHeight == null || (dataRowMinHeight == null && dataRowMaxHeight == null),
+         'dataRowHeight ($dataRowHeight) must not be set if dataRowMinHeight ($dataRowMinHeight) or dataRowMaxHeight ($dataRowMaxHeight) are set.'),
+       dataRowMinHeight = dataRowHeight ?? dataRowMinHeight,
+       dataRowMaxHeight = dataRowHeight ?? dataRowMaxHeight,
        _onlyTextColumn = _initOnlyTextColumn(columns);
 
   /// The configuration and labels for the columns in the table.
@@ -504,7 +515,29 @@
   /// If null, [DataTableThemeData.dataRowHeight] is used. This value defaults
   /// to [kMinInteractiveDimension] to adhere to the Material Design
   /// specifications.
-  final double? dataRowHeight;
+  @Deprecated(
+    'Migrate to use dataRowMinHeight and dataRowMaxHeight instead. '
+    'This feature was deprecated after v3.7.0-5.0.pre.',
+  )
+  double? get dataRowHeight => dataRowMinHeight == dataRowMaxHeight ? dataRowMinHeight : null;
+
+  /// {@template flutter.material.dataTable.dataRowMinHeight}
+  /// The minimum height of each row (excluding the row that contains column headings).
+  /// {@endtemplate}
+  ///
+  /// If null, [DataTableThemeData.dataRowMinHeight] is used. This value defaults
+  /// to [kMinInteractiveDimension] to adhere to the Material Design
+  /// specifications.
+  final double? dataRowMinHeight;
+
+  /// {@template flutter.material.dataTable.dataRowMaxHeight}
+  /// The maximum height of each row (excluding the row that contains column headings).
+  /// {@endtemplate}
+  ///
+  /// If null, [DataTableThemeData.dataRowMaxHeight] is used. This value defaults
+  /// to [kMinInteractiveDimension] to adhere to the Material Design
+  /// specifications.
+  final double? dataRowMaxHeight;
 
   /// {@template flutter.material.dataTable.dataTextStyle}
   /// The text style for data rows.
@@ -841,13 +874,17 @@
       ?? dataTableTheme.dataTextStyle
       ?? themeData.dataTableTheme.dataTextStyle
       ?? themeData.textTheme.bodyMedium!;
-    final double effectiveDataRowHeight = dataRowHeight
-      ?? dataTableTheme.dataRowHeight
-      ?? themeData.dataTableTheme.dataRowHeight
+    final double effectiveDataRowMinHeight = dataRowMinHeight
+      ?? dataTableTheme.dataRowMinHeight
+      ?? themeData.dataTableTheme.dataRowMinHeight
+      ?? kMinInteractiveDimension;
+    final double effectiveDataRowMaxHeight = dataRowMaxHeight
+      ?? dataTableTheme.dataRowMaxHeight
+      ?? themeData.dataTableTheme.dataRowMaxHeight
       ?? kMinInteractiveDimension;
     label = Container(
       padding: padding,
-      height: effectiveDataRowHeight,
+      constraints: BoxConstraints(minHeight: effectiveDataRowMinHeight, maxHeight: effectiveDataRowMaxHeight),
       alignment: numeric ? Alignment.centerRight : AlignmentDirectional.centerStart,
       child: DefaultTextStyle(
         style: effectiveDataTextStyle.copyWith(
@@ -1063,6 +1100,7 @@
         clipBehavior: clipBehavior,
         child: Table(
           columnWidths: tableColumns.asMap(),
+          defaultVerticalAlignment: TableCellVerticalAlignment.middle,
           children: tableRows,
           border: border,
         ),
diff --git a/framework/lib/src/material/data_table_theme.dart b/framework/lib/src/material/data_table_theme.dart
index 1ff4b55..7dbc975 100644
--- a/framework/lib/src/material/data_table_theme.dart
+++ b/framework/lib/src/material/data_table_theme.dart
@@ -40,7 +40,13 @@
   const DataTableThemeData({
     this.decoration,
     this.dataRowColor,
-    this.dataRowHeight,
+    @Deprecated(
+      'Migrate to use dataRowMinHeight and dataRowMaxHeight instead. '
+      'This feature was deprecated after v3.7.0-5.0.pre.',
+    )
+    double? dataRowHeight,
+    double? dataRowMinHeight,
+    double? dataRowMaxHeight,
     this.dataTextStyle,
     this.headingRowColor,
     this.headingRowHeight,
@@ -49,7 +55,11 @@
     this.columnSpacing,
     this.dividerThickness,
     this.checkboxHorizontalMargin,
-  });
+  }) : assert(dataRowMinHeight == null || dataRowMaxHeight == null || dataRowMaxHeight >= dataRowMinHeight),
+       assert(dataRowHeight == null || (dataRowMinHeight == null && dataRowMaxHeight == null),
+         'dataRowHeight ($dataRowHeight) must not be set if dataRowMinHeight ($dataRowMinHeight) or dataRowMaxHeight ($dataRowMaxHeight) are set.'),
+       dataRowMinHeight = dataRowHeight ?? dataRowMinHeight,
+       dataRowMaxHeight = dataRowHeight ?? dataRowMaxHeight;
 
   /// {@macro flutter.material.dataTable.decoration}
   final Decoration? decoration;
@@ -59,7 +69,17 @@
   final MaterialStateProperty<Color?>? dataRowColor;
 
   /// {@macro flutter.material.dataTable.dataRowHeight}
-  final double? dataRowHeight;
+  @Deprecated(
+    'Migrate to use dataRowMinHeight and dataRowMaxHeight instead. '
+    'This feature was deprecated after v3.7.0-5.0.pre.',
+  )
+  double? get dataRowHeight => dataRowMinHeight == dataRowMaxHeight ? dataRowMinHeight : null;
+
+  /// {@macro flutter.material.dataTable.dataRowMinHeight}
+  final double? dataRowMinHeight;
+
+  /// {@macro flutter.material.dataTable.dataRowMaxHeight}
+  final double? dataRowMaxHeight;
 
   /// {@macro flutter.material.dataTable.dataTextStyle}
   final TextStyle? dataTextStyle;
@@ -91,7 +111,13 @@
   DataTableThemeData copyWith({
     Decoration? decoration,
     MaterialStateProperty<Color?>? dataRowColor,
+    @Deprecated(
+      'Migrate to use dataRowMinHeight and dataRowMaxHeight instead. '
+      'This feature was deprecated after v3.7.0-5.0.pre.',
+    )
     double? dataRowHeight,
+    double? dataRowMinHeight,
+    double? dataRowMaxHeight,
     TextStyle? dataTextStyle,
     MaterialStateProperty<Color?>? headingRowColor,
     double? headingRowHeight,
@@ -105,6 +131,8 @@
       decoration: decoration ?? this.decoration,
       dataRowColor: dataRowColor ?? this.dataRowColor,
       dataRowHeight: dataRowHeight ?? this.dataRowHeight,
+      dataRowMinHeight: dataRowMinHeight ?? this.dataRowMinHeight,
+      dataRowMaxHeight: dataRowMaxHeight ?? this.dataRowMaxHeight,
       dataTextStyle: dataTextStyle ?? this.dataTextStyle,
       headingRowColor: headingRowColor ?? this.headingRowColor,
       headingRowHeight: headingRowHeight ?? this.headingRowHeight,
@@ -122,10 +150,14 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static DataTableThemeData lerp(DataTableThemeData a, DataTableThemeData b, double t) {
+    if (identical(a, b)) {
+      return a;
+    }
     return DataTableThemeData(
       decoration: Decoration.lerp(a.decoration, b.decoration, t),
       dataRowColor: MaterialStateProperty.lerp<Color?>(a.dataRowColor, b.dataRowColor, t, Color.lerp),
-      dataRowHeight: lerpDouble(a.dataRowHeight, b.dataRowHeight, t),
+      dataRowMinHeight: lerpDouble(a.dataRowMinHeight, b.dataRowMinHeight, t),
+      dataRowMaxHeight: lerpDouble(a.dataRowMaxHeight, b.dataRowMaxHeight, t),
       dataTextStyle: TextStyle.lerp(a.dataTextStyle, b.dataTextStyle, t),
       headingRowColor: MaterialStateProperty.lerp<Color?>(a.headingRowColor, b.headingRowColor, t, Color.lerp),
       headingRowHeight: lerpDouble(a.headingRowHeight, b.headingRowHeight, t),
@@ -141,7 +173,8 @@
   int get hashCode => Object.hash(
     decoration,
     dataRowColor,
-    dataRowHeight,
+    dataRowMinHeight,
+    dataRowMaxHeight,
     dataTextStyle,
     headingRowColor,
     headingRowHeight,
@@ -163,7 +196,8 @@
     return other is DataTableThemeData
       && other.decoration == decoration
       && other.dataRowColor == dataRowColor
-      && other.dataRowHeight == dataRowHeight
+      && other.dataRowMinHeight == dataRowMinHeight
+      && other.dataRowMaxHeight == dataRowMaxHeight
       && other.dataTextStyle == dataTextStyle
       && other.headingRowColor == headingRowColor
       && other.headingRowHeight == headingRowHeight
@@ -179,7 +213,8 @@
     super.debugFillProperties(properties);
     properties.add(DiagnosticsProperty<Decoration>('decoration', decoration, defaultValue: null));
     properties.add(DiagnosticsProperty<MaterialStateProperty<Color?>>('dataRowColor', dataRowColor, defaultValue: null));
-    properties.add(DoubleProperty('dataRowHeight', dataRowHeight, defaultValue: null));
+    properties.add(DoubleProperty('dataRowMinHeight', dataRowMinHeight, defaultValue: null));
+    properties.add(DoubleProperty('dataRowMaxHeight', dataRowMaxHeight, defaultValue: null));
     properties.add(DiagnosticsProperty<TextStyle>('dataTextStyle', dataTextStyle, defaultValue: null));
     properties.add(DiagnosticsProperty<MaterialStateProperty<Color?>>('headingRowColor', headingRowColor, defaultValue: null));
     properties.add(DoubleProperty('headingRowHeight', headingRowHeight, defaultValue: null));
diff --git a/framework/lib/src/material/date_picker.dart b/framework/lib/src/material/date_picker.dart
index e9e0c90..ac032e7 100644
--- a/framework/lib/src/material/date_picker.dart
+++ b/framework/lib/src/material/date_picker.dart
@@ -164,6 +164,7 @@
   String? fieldLabelText,
   TextInputType? keyboardType,
   Offset? anchorPoint,
+  final ValueChanged<DatePickerEntryMode>? onDatePickerModeChange
 }) async {
   initialDate = DateUtils.dateOnly(initialDate);
   firstDate = DateUtils.dateOnly(firstDate);
@@ -202,6 +203,7 @@
     fieldHintText: fieldHintText,
     fieldLabelText: fieldLabelText,
     keyboardType: keyboardType,
+    onDatePickerModeChange: onDatePickerModeChange,
   );
 
   if (textDirection != null) {
@@ -259,6 +261,7 @@
     this.fieldLabelText,
     this.keyboardType,
     this.restorationId,
+    this.onDatePickerModeChange
   }) : initialDate = DateUtils.dateOnly(initialDate),
        firstDate = DateUtils.dateOnly(firstDate),
        lastDate = DateUtils.dateOnly(lastDate),
@@ -356,6 +359,15 @@
   ///    Flutter.
   final String? restorationId;
 
+
+  /// Called when the [DatePickerDialog] is toggled between
+  /// [DatePickerEntryMode.calendar],[DatePickerEntryMode.input].
+  ///
+  /// An example of how this callback might be used is an app that saves the
+  /// user's preferred entry mode and uses it to initialize the
+  /// `initialEntryMode` parameter the next time the date picker is shown.
+  final ValueChanged<DatePickerEntryMode>? onDatePickerModeChange;
+
   @override
   State<DatePickerDialog> createState() => _DatePickerDialogState();
 }
@@ -394,16 +406,24 @@
     Navigator.pop(context);
   }
 
+  void _handleOnDatePickerModeChange() {
+    if (widget.onDatePickerModeChange != null) {
+      widget.onDatePickerModeChange!(_entryMode.value);
+    }
+  }
+
   void _handleEntryModeToggle() {
     setState(() {
       switch (_entryMode.value) {
         case DatePickerEntryMode.calendar:
           _autovalidateMode.value = AutovalidateMode.disabled;
           _entryMode.value = DatePickerEntryMode.input;
+          _handleOnDatePickerModeChange();
           break;
         case DatePickerEntryMode.input:
           _formKey.currentState!.save();
           _entryMode.value = DatePickerEntryMode.calendar;
+           _handleOnDatePickerModeChange();
           break;
         case DatePickerEntryMode.calendarOnly:
         case DatePickerEntryMode.inputOnly:
@@ -467,7 +487,7 @@
           color: headerForegroundColor,
         )
       // Material2 has support for landscape and the current M3 spec doesn't
-      // address this layout, so handling it seperately here.
+      // address this layout, so handling it separately here.
       : (orientation == Orientation.landscape
         ? textTheme.headlineSmall?.copyWith(color: headerForegroundColor)
         : textTheme.headlineMedium?.copyWith(color: headerForegroundColor));
diff --git a/framework/lib/src/material/date_picker_theme.dart b/framework/lib/src/material/date_picker_theme.dart
index c9a394f..98a15a1 100644
--- a/framework/lib/src/material/date_picker_theme.dart
+++ b/framework/lib/src/material/date_picker_theme.dart
@@ -152,7 +152,7 @@
   /// day labels in the grid of the date picker.
   final MaterialStateProperty<Color?>? dayBackgroundColor;
 
-  /// Overriddes the default highlight color that's typically used to
+  /// Overrides the default highlight color that's typically used to
   /// indicate that a day in the grid is focused, hovered, or pressed.
   final MaterialStateProperty<Color?>? dayOverlayColor;
 
@@ -356,6 +356,9 @@
 
   /// Linearly interpolates between two [DatePickerThemeData].
   static DatePickerThemeData lerp(DatePickerThemeData? a, DatePickerThemeData? b, double t) {
+    if (identical(a, b) && a != null) {
+      return a;
+    }
     return DatePickerThemeData(
       backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t),
       elevation: lerpDouble(a?.elevation, b?.elevation, t),
@@ -393,8 +396,8 @@
   }
 
   static BorderSide? _lerpBorderSide(BorderSide? a, BorderSide? b, double t) {
-    if (a == null && b == null) {
-      return null;
+    if (identical(a, b)) {
+      return a;
     }
     if (a == null) {
       return BorderSide.lerp(BorderSide(width: 0, color: b!.color.withAlpha(0)), b, t);
diff --git a/framework/lib/src/material/dialog.dart b/framework/lib/src/material/dialog.dart
index 414e8d7..99a1bee 100644
--- a/framework/lib/src/material/dialog.dart
+++ b/framework/lib/src/material/dialog.dart
@@ -482,9 +482,10 @@
   /// Typically used to provide padding to the button bar between the button bar
   /// and the edges of the dialog.
   ///
-  /// If there are no [actions], then no padding will be included. It is also
-  /// important to note that [buttonPadding] may contribute to the padding on
-  /// the edges of [actions] as well.
+  /// The [buttonPadding] may contribute to the padding on the edges of
+  /// [actions] as well.
+  ///
+  /// If there are no [actions], then no padding will be included.
   ///
   /// {@tool snippet}
   /// This is an example of a set of actions aligned with the content widget.
@@ -546,21 +547,20 @@
   /// * [OverflowBar], which [actions] configures to lay itself out.
   final VerticalDirection? actionsOverflowDirection;
 
-  /// The spacing between [actions] when the [OverflowBar] switches
-  /// to a column layout because the actions don't fit horizontally.
+  /// The spacing between [actions] when the [OverflowBar] switches to a column
+  /// layout because the actions don't fit horizontally.
   ///
   /// If the widgets in [actions] do not fit into a single row, they are
-  /// arranged into a column. This parameter provides additional
-  /// vertical space in between buttons when it does overflow.
+  /// arranged into a column. This parameter provides additional vertical space
+  /// between buttons when it does overflow.
   ///
-  /// Note that the button spacing may appear to be more than
-  /// the value provided. This is because most buttons adhere to the
-  /// [MaterialTapTargetSize] of 48px. So, even though a button
-  /// might visually be 36px in height, it might still take up to
-  /// 48px vertically.
+  /// The button spacing may appear to be more than the value provided. This is
+  /// because most buttons adhere to the [MaterialTapTargetSize] of 48px. So,
+  /// even though a button might visually be 36px in height, it might still take
+  /// up to 48px vertically.
   ///
-  /// If null then no spacing will be added in between buttons in
-  /// an overflow state.
+  /// If null then no spacing will be added in between buttons in an overflow
+  /// state.
   final double? actionsOverflowButtonSpacing;
 
   /// The padding that surrounds each button in [actions].
diff --git a/framework/lib/src/material/dialog_theme.dart b/framework/lib/src/material/dialog_theme.dart
index c39da59..2a7c78a 100644
--- a/framework/lib/src/material/dialog_theme.dart
+++ b/framework/lib/src/material/dialog_theme.dart
@@ -111,6 +111,9 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static DialogTheme lerp(DialogTheme? a, DialogTheme? b, double t) {
+    if (identical(a, b) && a != null) {
+      return a;
+    }
     return DialogTheme(
       backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t),
       elevation: lerpDouble(a?.elevation, b?.elevation, t),
diff --git a/framework/lib/src/material/divider_theme.dart b/framework/lib/src/material/divider_theme.dart
index 79d631d..58fd915 100644
--- a/framework/lib/src/material/divider_theme.dart
+++ b/framework/lib/src/material/divider_theme.dart
@@ -87,6 +87,9 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static DividerThemeData lerp(DividerThemeData? a, DividerThemeData? b, double t) {
+    if (identical(a, b) && a != null) {
+      return a;
+    }
     return DividerThemeData(
       color: Color.lerp(a?.color, b?.color, t),
       space: lerpDouble(a?.space, b?.space, t),
diff --git a/framework/lib/src/material/drawer_theme.dart b/framework/lib/src/material/drawer_theme.dart
index 6986893..269af00 100644
--- a/framework/lib/src/material/drawer_theme.dart
+++ b/framework/lib/src/material/drawer_theme.dart
@@ -99,8 +99,8 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static DrawerThemeData? lerp(DrawerThemeData? a, DrawerThemeData? b, double t) {
-    if (a == null && b == null) {
-      return null;
+    if (identical(a, b)) {
+      return a;
     }
     return DrawerThemeData(
       backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t),
diff --git a/framework/lib/src/material/dropdown.dart b/framework/lib/src/material/dropdown.dart
index 4286f51..29637b8 100644
--- a/framework/lib/src/material/dropdown.dart
+++ b/framework/lib/src/material/dropdown.dart
@@ -886,6 +886,7 @@
     this.enableFeedback,
     this.alignment = AlignmentDirectional.centerStart,
     this.borderRadius,
+    this.padding,
     // When adding new arguments, consider adding similar arguments to
     // DropdownButtonFormField.
   }) : assert(items == null || items.isEmpty || value == null ||
@@ -929,6 +930,7 @@
     this.enableFeedback,
     this.alignment = AlignmentDirectional.centerStart,
     this.borderRadius,
+    this.padding,
     required InputDecoration inputDecoration,
     required bool isEmpty,
     required bool isFocused,
@@ -1115,6 +1117,17 @@
   /// instead.
   final Color? dropdownColor;
 
+  /// Padding around the visible portion of the dropdown widget.
+  ///
+  /// As the padding increases, the size of the [DropdownButton] will also
+  /// increase. The padding is included in the clickable area of the dropdown
+  /// widget, so this can make the widget easier to click.
+  ///
+  /// Padding can be useful when used with a custom border. The clickable
+  /// area will stay flush with the border, as opposed to an external [Padding]
+  /// widget which will leave a non-clickable gap.
+  final EdgeInsetsGeometry? padding;
+
   /// The maximum height of the menu.
   ///
   /// The maximum height of the menu must be at least one row shorter than
@@ -1505,7 +1518,7 @@
           autofocus: widget.autofocus,
           focusColor: widget.focusColor ?? Theme.of(context).focusColor,
           enableFeedback: false,
-          child: result,
+          child: widget.padding == null ? result : Padding(padding: widget.padding!, child: result),
         ),
       ),
     );
@@ -1566,6 +1579,7 @@
     bool? enableFeedback,
     AlignmentGeometry alignment = AlignmentDirectional.centerStart,
     BorderRadius? borderRadius,
+    EdgeInsetsGeometry? padding,
     // When adding new arguments, consider adding similar arguments to
     // DropdownButton.
   }) : assert(items == null || items.isEmpty || value == null ||
@@ -1635,6 +1649,7 @@
                    inputDecoration: effectiveDecoration.copyWith(errorText: field.errorText),
                    isEmpty: isEmpty,
                    isFocused: Focus.of(context).hasFocus,
+                   padding: padding,
                  ),
                );
              }),
diff --git a/framework/lib/src/material/dropdown_menu.dart b/framework/lib/src/material/dropdown_menu.dart
index a4d5ab9..d2e0632 100644
--- a/framework/lib/src/material/dropdown_menu.dart
+++ b/framework/lib/src/material/dropdown_menu.dart
@@ -498,6 +498,8 @@
       ?? theme.inputDecorationTheme
       ?? defaults.inputDecorationTheme!;
 
+    final MouseCursor effectiveMouseCursor = canRequestFocus() ? SystemMouseCursors.text : SystemMouseCursors.click;
+
     return Shortcuts(
       shortcuts: _kMenuTraversalShortcuts,
       child: Actions(
@@ -539,6 +541,7 @@
               width: widget.width,
               children: <Widget>[
                 TextField(
+                  mouseCursor: effectiveMouseCursor,
                   canRequestFocus: canRequestFocus(),
                   enableInteractiveSelection: canRequestFocus(),
                   textAlignVertical: TextAlignVertical.center,
diff --git a/framework/lib/src/material/dropdown_menu_theme.dart b/framework/lib/src/material/dropdown_menu_theme.dart
index 7d891c8..38a1d91 100644
--- a/framework/lib/src/material/dropdown_menu_theme.dart
+++ b/framework/lib/src/material/dropdown_menu_theme.dart
@@ -64,6 +64,9 @@
 
   /// Linearly interpolates between two dropdown menu themes.
   static DropdownMenuThemeData lerp(DropdownMenuThemeData? a, DropdownMenuThemeData? b, double t) {
+    if (identical(a, b) && a != null) {
+      return a;
+    }
     return DropdownMenuThemeData(
       textStyle: TextStyle.lerp(a?.textStyle, b?.textStyle, t),
       inputDecorationTheme: t < 0.5 ? a?.inputDecorationTheme : b?.inputDecorationTheme,
diff --git a/framework/lib/src/material/elevated_button.dart b/framework/lib/src/material/elevated_button.dart
index 2ba8c92..97e8a45 100644
--- a/framework/lib/src/material/elevated_button.dart
+++ b/framework/lib/src/material/elevated_button.dart
@@ -333,10 +333,10 @@
   ///   * hovered - 3
   ///   * focused or pressed - 1
   /// * `padding`
-  ///   * `textScaleFactor <= 1` - horizontal(16)
-  ///   * `1 < textScaleFactor <= 2` - lerp(horizontal(16), horizontal(8))
-  ///   * `2 < textScaleFactor <= 3` - lerp(horizontal(8), horizontal(4))
-  ///   * `3 < textScaleFactor` - horizontal(4)
+  ///   * `textScaleFactor <= 1` - horizontal(24)
+  ///   * `1 < textScaleFactor <= 2` - lerp(horizontal(24), horizontal(12))
+  ///   * `2 < textScaleFactor <= 3` - lerp(horizontal(12), horizontal(6))
+  ///   * `3 < textScaleFactor` - horizontal(6)
   /// * `minimumSize` - Size(64, 40)
   /// * `fixedSize` - null
   /// * `maximumSize` - Size.infinite
@@ -351,6 +351,10 @@
   /// * `enableFeedback` - true
   /// * `alignment` - Alignment.center
   /// * `splashFactory` - Theme.splashFactory
+  ///
+  /// For the [ElevatedButton.icon] factory, the start (generally the left) value of
+  /// [padding] is reduced from 24 to 16.
+
   @override
   ButtonStyle defaultStyleOf(BuildContext context) {
     final ThemeData theme = Theme.of(context);
@@ -390,10 +394,12 @@
 }
 
 EdgeInsetsGeometry _scaledPadding(BuildContext context) {
+  final bool useMaterial3 = Theme.of(context).useMaterial3;
+  final double padding1x = useMaterial3 ? 24.0 : 16.0;
   return ButtonStyleButton.scaledPadding(
-    const EdgeInsets.symmetric(horizontal: 16),
-    const EdgeInsets.symmetric(horizontal: 8),
-    const EdgeInsets.symmetric(horizontal: 4),
+     EdgeInsets.symmetric(horizontal: padding1x),
+     EdgeInsets.symmetric(horizontal: padding1x / 2),
+     EdgeInsets.symmetric(horizontal: padding1x / 2 / 2),
     MediaQuery.textScaleFactorOf(context),
   );
 }
@@ -494,7 +500,13 @@
 
   @override
   ButtonStyle defaultStyleOf(BuildContext context) {
-    final EdgeInsetsGeometry scaledPadding = ButtonStyleButton.scaledPadding(
+    final bool useMaterial3 = Theme.of(context).useMaterial3;
+    final EdgeInsetsGeometry scaledPadding = useMaterial3 ?  ButtonStyleButton.scaledPadding(
+      const EdgeInsetsDirectional.fromSTEB(16, 0, 24, 0),
+      const EdgeInsetsDirectional.fromSTEB(8, 0, 12, 0),
+      const EdgeInsetsDirectional.fromSTEB(4, 0, 6, 0),
+      MediaQuery.textScaleFactorOf(context),
+    ) : ButtonStyleButton.scaledPadding(
       const EdgeInsetsDirectional.fromSTEB(12, 0, 16, 0),
       const EdgeInsets.symmetric(horizontal: 8),
       const EdgeInsetsDirectional.fromSTEB(8, 0, 4, 0),
diff --git a/framework/lib/src/material/elevated_button_theme.dart b/framework/lib/src/material/elevated_button_theme.dart
index bc59beb..cff96c6 100644
--- a/framework/lib/src/material/elevated_button_theme.dart
+++ b/framework/lib/src/material/elevated_button_theme.dart
@@ -49,8 +49,8 @@
 
   /// Linearly interpolate between two elevated button themes.
   static ElevatedButtonThemeData? lerp(ElevatedButtonThemeData? a, ElevatedButtonThemeData? b, double t) {
-    if (a == null && b == null) {
-      return null;
+    if (identical(a, b)) {
+      return a;
     }
     return ElevatedButtonThemeData(
       style: ButtonStyle.lerp(a?.style, b?.style, t),
diff --git a/framework/lib/src/material/expansion_panel.dart b/framework/lib/src/material/expansion_panel.dart
index f318c6b..f558fbc 100644
--- a/framework/lib/src/material/expansion_panel.dart
+++ b/framework/lib/src/material/expansion_panel.dart
@@ -118,7 +118,6 @@
 ///
 /// See [ExpansionPanelList.radio] for a sample implementation.
 class ExpansionPanelRadio extends ExpansionPanel {
-
   /// An expansion panel that allows for radio functionality.
   ///
   /// A unique [value] must be passed into the constructor. The
@@ -139,19 +138,24 @@
 /// A material expansion panel list that lays out its children and animates
 /// expansions.
 ///
-/// Note that [expansionCallback] behaves differently for [ExpansionPanelList]
-/// and [ExpansionPanelList.radio].
+/// The [expansionCallback] is called when the expansion state changes. For
+/// normal [ExpansionPanelList] widgets, it is the responsibility of the parent
+/// widget to rebuild the [ExpansionPanelList] with updated values for
+/// [ExpansionPanel.isExpanded]. For [ExpansionPanelList.radio] widgets, the
+/// open state is tracked internally and the callback is invoked both for the
+/// previously open panel, which is closing, and the previously closed panel,
+/// which is opening.
 ///
 /// {@tool dartpad}
-/// Here is a simple example of how to implement ExpansionPanelList.
+/// Here is a simple example of how to use [ExpansionPanelList].
 ///
 /// ** See code in examples/api/lib/material/expansion_panel/expansion_panel_list.0.dart **
 /// {@end-tool}
 ///
 /// See also:
 ///
-///  * [ExpansionPanel]
-///  * [ExpansionPanelList.radio]
+///  * [ExpansionPanel], which is used in the [children] property.
+///  * [ExpansionPanelList.radio], a variant of this widget where only one panel is open at a time.
 ///  * <https://material.io/design/components/lists.html#types>
 class ExpansionPanelList extends StatefulWidget {
   /// Creates an expansion panel list widget. The [expansionCallback] is
@@ -208,10 +212,11 @@
   /// passed to the second callback are the index of the panel that will close
   /// and false, marking that it will be closed.
   ///
-  /// For [ExpansionPanelList], the callback needs to setState when it's notified
-  /// about the closing/opening panel. On the other hand, the callback for
-  /// [ExpansionPanelList.radio] is intended to inform the parent widget of
-  /// changes, as the radio panels' open/close states are managed internally.
+  /// For [ExpansionPanelList], the callback should call [State.setState] when
+  /// it is notified about the closing/opening panel. On the other hand, the
+  /// callback for [ExpansionPanelList.radio] is intended to inform the parent
+  /// widget of changes, as the radio panels' open/close states are managed
+  /// internally.
   ///
   /// This callback is useful in order to keep track of the expanded/collapsed
   /// panels in a parent widget that may need to react to these changes.
diff --git a/framework/lib/src/material/expansion_tile.dart b/framework/lib/src/material/expansion_tile.dart
index 0ec82f8..2829711 100644
--- a/framework/lib/src/material/expansion_tile.dart
+++ b/framework/lib/src/material/expansion_tile.dart
@@ -34,7 +34,8 @@
 /// to the [leading] and [trailing] properties of [ExpansionTile].
 ///
 /// {@tool dartpad}
-/// This example demonstrates different configurations of ExpansionTile.
+/// This example demonstrates how the [ExpansionTile] icon's location and appearance
+/// can be customized.
 ///
 /// ** See code in examples/api/lib/material/expansion_tile/expansion_tile.0.dart **
 /// {@end-tool}
@@ -83,7 +84,7 @@
   ///
   /// Typically a [CircleAvatar] widget.
   ///
-  /// Note that depending on the value of [controlAffinity], the [leading] widget
+  /// Depending on the value of [controlAffinity], the [leading] widget
   /// may replace the rotating expansion arrow icon.
   final Widget? leading;
 
@@ -133,7 +134,7 @@
 
   /// A widget to display after the title.
   ///
-  /// Note that depending on the value of [controlAffinity], the [trailing] widget
+  /// Depending on the value of [controlAffinity], the [trailing] widget
   /// may replace the rotating expansion arrow icon.
   final Widget? trailing;
 
@@ -187,17 +188,19 @@
   /// Specifies the alignment of each child within [children] when the tile is expanded.
   ///
   /// The internals of the expanded tile make use of a [Column] widget for
-  /// [children], and the `crossAxisAlignment` parameter is passed directly into the [Column].
+  /// [children], and the `crossAxisAlignment` parameter is passed directly into
+  /// the [Column].
   ///
   /// Modifying this property controls the cross axis alignment of each child
-  /// within its [Column]. Note that the width of the [Column] that houses
-  /// [children] will be the same as the widest child widget in [children]. It is
-  /// not necessarily the width of [Column] is equal to the width of expanded tile.
+  /// within its [Column]. The width of the [Column] that houses [children] will
+  /// be the same as the widest child widget in [children]. The width of the
+  /// [Column] might not be equal to the width of the expanded tile.
   ///
-  /// To align the [Column] along the expanded tile, use the [expandedAlignment] property
-  /// instead.
+  /// To align the [Column] along the expanded tile, use the [expandedAlignment]
+  /// property instead.
   ///
-  /// When the value is null, the value of [expandedCrossAxisAlignment] is [CrossAxisAlignment.center].
+  /// When the value is null, the value of [expandedCrossAxisAlignment] is
+  /// [CrossAxisAlignment.center].
   final CrossAxisAlignment? expandedCrossAxisAlignment;
 
   /// Specifies padding for [children].
@@ -216,7 +219,7 @@
   /// Used to override to the [ListTileThemeData.iconColor].
   ///
   /// If this property is null then [ExpansionTileThemeData.iconColor] is used. If that
-  /// is also null then the value of [ListTileThemeData.iconColor] is used.
+  /// is also null then the value of [ColorScheme.primary] is used.
   ///
   /// See also:
   ///
@@ -227,6 +230,15 @@
   /// The icon color of tile's expansion arrow icon when the sublist is collapsed.
   ///
   /// Used to override to the [ListTileThemeData.iconColor].
+  ///
+  /// If this property is null then [ExpansionTileThemeData.collapsedIconColor] is used. If that
+  /// is also null and [ThemeData.useMaterial3] is true, [ColorScheme.onSurface] is used. Otherwise,
+  /// defaults to [ThemeData.unselectedWidgetColor] color.
+  ///
+  /// See also:
+  ///
+  /// * [ExpansionTileTheme.of], which returns the nearest [ExpansionTileTheme]'s
+  ///   [ExpansionTileThemeData].
   final Color? collapsedIconColor;
 
 
@@ -235,7 +247,8 @@
   /// Used to override to the [ListTileThemeData.textColor].
   ///
   /// If this property is null then [ExpansionTileThemeData.textColor] is used. If that
-  /// is also null then the value of [ListTileThemeData.textColor] is used.
+  /// is also null then and [ThemeData.useMaterial3] is true, color of the [TextTheme.bodyLarge]
+  /// will be used for the [title] and [subtitle]. Otherwise, defaults to [ColorScheme.primary] color.
   ///
   /// See also:
   ///
@@ -247,8 +260,10 @@
   ///
   /// Used to override to the [ListTileThemeData.textColor].
   ///
-  /// If this property is null then [ExpansionTileThemeData.collapsedTextColor] is used. If that
-  /// is also null then the value of [ListTileThemeData.textColor] is used.
+  /// If this property is null then [ExpansionTileThemeData.collapsedTextColor] is used.
+  /// If that is also null and [ThemeData.useMaterial3] is true, color of the
+  /// [TextTheme.bodyLarge] will be used for the [title] and [subtitle]. Otherwise,
+  /// defaults to color of the [TextTheme.titleMedium].
   ///
   /// See also:
   ///
@@ -441,7 +456,9 @@
   void didChangeDependencies() {
     final ThemeData theme = Theme.of(context);
     final ExpansionTileThemeData expansionTileTheme = ExpansionTileTheme.of(context);
-    final ColorScheme colorScheme = theme.colorScheme;
+    final ExpansionTileThemeData defaults = theme.useMaterial3
+      ? _ExpansionTileDefaultsM3(context)
+      : _ExpansionTileDefaultsM2(context);
     _borderTween
       ..begin = widget.collapsedShape
         ?? expansionTileTheme.collapsedShape
@@ -458,13 +475,13 @@
     _headerColorTween
       ..begin = widget.collapsedTextColor
         ?? expansionTileTheme.collapsedTextColor
-        ?? theme.textTheme.titleMedium!.color
-      ..end = widget.textColor ?? expansionTileTheme.textColor ?? colorScheme.primary;
+        ?? defaults.collapsedTextColor
+      ..end = widget.textColor ?? expansionTileTheme.textColor ?? defaults.textColor;
     _iconColorTween
       ..begin = widget.collapsedIconColor
         ?? expansionTileTheme.collapsedIconColor
-        ?? theme.unselectedWidgetColor
-      ..end = widget.iconColor ?? expansionTileTheme.iconColor ?? colorScheme.primary;
+        ?? defaults.collapsedIconColor
+      ..end = widget.iconColor ?? expansionTileTheme.iconColor ?? defaults.iconColor;
     _backgroundColorTween
       ..begin = widget.collapsedBackgroundColor ?? expansionTileTheme.collapsedBackgroundColor
       ..end = widget.backgroundColor ?? expansionTileTheme.backgroundColor;
@@ -498,3 +515,54 @@
     );
   }
 }
+
+class _ExpansionTileDefaultsM2 extends ExpansionTileThemeData {
+  _ExpansionTileDefaultsM2(this.context);
+
+  final BuildContext context;
+  late final ThemeData _theme = Theme.of(context);
+  late final ColorScheme _colorScheme = _theme.colorScheme;
+
+  @override
+  Color? get textColor => _colorScheme.primary;
+
+  @override
+  Color? get iconColor => _colorScheme.primary;
+
+  @override
+  Color? get collapsedTextColor => _theme.textTheme.titleMedium!.color;
+
+  @override
+  Color? get collapsedIconColor => _theme.unselectedWidgetColor;
+}
+
+// BEGIN GENERATED TOKEN PROPERTIES - ExpansionTile
+
+// Do not edit by hand. The code between the "BEGIN GENERATED" and
+// "END GENERATED" comments are generated from data in the Material
+// Design token database by the script:
+//   dev/tools/gen_defaults/bin/gen_defaults.dart.
+
+// Token database version: v0_158
+
+class _ExpansionTileDefaultsM3 extends ExpansionTileThemeData {
+  _ExpansionTileDefaultsM3(this.context);
+
+  final BuildContext context;
+  late final ThemeData _theme = Theme.of(context);
+  late final ColorScheme _colors = _theme.colorScheme;
+
+  @override
+  Color? get textColor => _colors.onSurface;
+
+  @override
+  Color? get iconColor => _colors.primary;
+
+  @override
+  Color? get collapsedTextColor => _colors.onSurface;
+
+  @override
+  Color? get collapsedIconColor => _colors.onSurfaceVariant;
+}
+
+// END GENERATED TOKEN PROPERTIES - ExpansionTile
diff --git a/framework/lib/src/material/expansion_tile_theme.dart b/framework/lib/src/material/expansion_tile_theme.dart
index 8d62465..534ffe7 100644
--- a/framework/lib/src/material/expansion_tile_theme.dart
+++ b/framework/lib/src/material/expansion_tile_theme.dart
@@ -124,8 +124,8 @@
 
   /// Linearly interpolate between ExpansionTileThemeData objects.
   static ExpansionTileThemeData? lerp(ExpansionTileThemeData? a, ExpansionTileThemeData? b, double t) {
-    if (a == null && b == null) {
-      return null;
+    if (identical(a, b)) {
+      return a;
     }
     return ExpansionTileThemeData(
       backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t),
diff --git a/framework/lib/src/material/filled_button.dart b/framework/lib/src/material/filled_button.dart
index a7f52b6..2548eea 100644
--- a/framework/lib/src/material/filled_button.dart
+++ b/framework/lib/src/material/filled_button.dart
@@ -345,6 +345,50 @@
   /// shape's [OutlinedBorder.side]. Typically the default value of an
   /// [OutlinedBorder]'s side is [BorderSide.none], so an outline is not drawn.
   ///
+  /// ## Material 3 defaults
+  ///
+  /// If [ThemeData.useMaterial3] is set to true the following defaults will
+  /// be used:
+  ///
+  /// * `textStyle` - Theme.textTheme.labelLarge
+  /// * `backgroundColor`
+  ///   * disabled - Theme.colorScheme.onSurface(0.12)
+  ///   * others - Theme.colorScheme.secondaryContainer
+  /// * `foregroundColor`
+  ///   * disabled - Theme.colorScheme.onSurface(0.38)
+  ///   * others - Theme.colorScheme.onSecondaryContainer
+  /// * `overlayColor`
+  ///   * hovered - Theme.colorScheme.onSecondaryContainer(0.08)
+  ///   * focused or pressed - Theme.colorScheme.onSecondaryContainer(0.12)
+  /// * `shadowColor` - Theme.colorScheme.shadow
+  /// * `surfaceTintColor` - Colors.transparent
+  /// * `elevation`
+  ///   * disabled - 0
+  ///   * default - 1
+  ///   * hovered - 3
+  ///   * focused or pressed - 1
+  /// * `padding`
+  ///   * `textScaleFactor <= 1` - horizontal(24)
+  ///   * `1 < textScaleFactor <= 2` - lerp(horizontal(24), horizontal(12))
+  ///   * `2 < textScaleFactor <= 3` - lerp(horizontal(12), horizontal(6))
+  ///   * `3 < textScaleFactor` - horizontal(6)
+  /// * `minimumSize` - Size(64, 40)
+  /// * `fixedSize` - null
+  /// * `maximumSize` - Size.infinite
+  /// * `side` - null
+  /// * `shape` - StadiumBorder()
+  /// * `mouseCursor`
+  ///   * disabled - SystemMouseCursors.basic
+  ///   * others - SystemMouseCursors.click
+  /// * `visualDensity` - Theme.visualDensity
+  /// * `tapTargetSize` - Theme.materialTapTargetSize
+  /// * `animationDuration` - kThemeChangeDuration
+  /// * `enableFeedback` - true
+  /// * `alignment` - Alignment.center
+  /// * `splashFactory` - Theme.splashFactory
+  ///
+  /// For the [FilledButton.icon] factory, the start (generally the left) value of
+  /// [padding] is reduced from 24 to 16.
   @override
   ButtonStyle defaultStyleOf(BuildContext context) {
     switch (_variant) {
@@ -364,10 +408,12 @@
 }
 
 EdgeInsetsGeometry _scaledPadding(BuildContext context) {
+  final bool useMaterial3 = Theme.of(context).useMaterial3;
+  final double padding1x = useMaterial3 ? 24.0 : 16.0;
   return ButtonStyleButton.scaledPadding(
-    const EdgeInsets.symmetric(horizontal: 16),
-    const EdgeInsets.symmetric(horizontal: 8),
-    const EdgeInsets.symmetric(horizontal: 4),
+     EdgeInsets.symmetric(horizontal: padding1x),
+     EdgeInsets.symmetric(horizontal: padding1x / 2),
+     EdgeInsets.symmetric(horizontal: padding1x / 2 / 2),
     MediaQuery.textScaleFactorOf(context),
   );
 }
@@ -463,7 +509,13 @@
 
   @override
   ButtonStyle defaultStyleOf(BuildContext context) {
-    final EdgeInsetsGeometry scaledPadding = ButtonStyleButton.scaledPadding(
+    final bool useMaterial3 = Theme.of(context).useMaterial3;
+    final EdgeInsetsGeometry scaledPadding = useMaterial3 ?  ButtonStyleButton.scaledPadding(
+      const EdgeInsetsDirectional.fromSTEB(16, 0, 24, 0),
+      const EdgeInsetsDirectional.fromSTEB(8, 0, 12, 0),
+      const EdgeInsetsDirectional.fromSTEB(4, 0, 6, 0),
+      MediaQuery.textScaleFactorOf(context),
+    ) : ButtonStyleButton.scaledPadding(
       const EdgeInsetsDirectional.fromSTEB(12, 0, 16, 0),
       const EdgeInsets.symmetric(horizontal: 8),
       const EdgeInsetsDirectional.fromSTEB(8, 0, 4, 0),
diff --git a/framework/lib/src/material/filled_button_theme.dart b/framework/lib/src/material/filled_button_theme.dart
index 8e3a4d3..4e45800 100644
--- a/framework/lib/src/material/filled_button_theme.dart
+++ b/framework/lib/src/material/filled_button_theme.dart
@@ -49,8 +49,8 @@
 
   /// Linearly interpolate between two filled button themes.
   static FilledButtonThemeData? lerp(FilledButtonThemeData? a, FilledButtonThemeData? b, double t) {
-    if (a == null && b == null) {
-      return null;
+    if (identical(a, b)) {
+      return a;
     }
     return FilledButtonThemeData(
       style: ButtonStyle.lerp(a?.style, b?.style, t),
diff --git a/framework/lib/src/material/floating_action_button_theme.dart b/framework/lib/src/material/floating_action_button_theme.dart
index 1c744d5..0b656e6 100644
--- a/framework/lib/src/material/floating_action_button_theme.dart
+++ b/framework/lib/src/material/floating_action_button_theme.dart
@@ -193,8 +193,8 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static FloatingActionButtonThemeData? lerp(FloatingActionButtonThemeData? a, FloatingActionButtonThemeData? b, double t) {
-    if (a == null && b == null) {
-      return null;
+    if (identical(a, b)) {
+      return a;
     }
     return FloatingActionButtonThemeData(
       foregroundColor: Color.lerp(a?.foregroundColor, b?.foregroundColor, t),
diff --git a/framework/lib/src/material/icon_button.dart b/framework/lib/src/material/icon_button.dart
index 21f541b..e707e13 100644
--- a/framework/lib/src/material/icon_button.dart
+++ b/framework/lib/src/material/icon_button.dart
@@ -30,6 +30,8 @@
 // See: <https://material.io/design/usability/accessibility.html#layout-typography>.
 const double _kMinButtonSize = kMinInteractiveDimension;
 
+enum _IconButtonVariant { standard, filled, filledTonal, outlined }
+
 /// A Material Design icon button.
 ///
 /// An icon button is a picture printed on a [Material] widget that reacts to
@@ -115,8 +117,9 @@
 /// {@end-tool}
 ///
 /// Material Design 3 introduced new types (standard and contained) of [IconButton]s.
-/// The default [IconButton] is the standard type, and contained icon buttons can be produced
-/// by configuring the [IconButton] widget's properties.
+/// The default [IconButton] is the standard type. To create a filled icon button,
+/// use [IconButton.filled]; to create a filled tonal icon button, use [IconButton.filledTonal];
+/// to create a outlined icon button, use [IconButton.outlined].
 ///
 /// Material Design 3 also treats [IconButton]s as toggle buttons. In order
 /// to not break existing apps, the toggle feature can be optionally controlled
@@ -198,7 +201,104 @@
     this.isSelected,
     this.selectedIcon,
     required this.icon,
-  }) : assert(splashRadius == null || splashRadius > 0);
+  }) : assert(splashRadius == null || splashRadius > 0),
+       _variant = _IconButtonVariant.standard;
+
+  /// Create a filled variant of IconButton.
+  ///
+  /// Filled icon buttons have higher visual impact and should be used for
+  /// high emphasis actions, such as turning off a microphone or camera.
+  const IconButton.filled({
+    super.key,
+    this.iconSize,
+    this.visualDensity,
+    this.padding,
+    this.alignment,
+    this.splashRadius,
+    this.color,
+    this.focusColor,
+    this.hoverColor,
+    this.highlightColor,
+    this.splashColor,
+    this.disabledColor,
+    required this.onPressed,
+    this.mouseCursor,
+    this.focusNode,
+    this.autofocus = false,
+    this.tooltip,
+    this.enableFeedback,
+    this.constraints,
+    this.style,
+    this.isSelected,
+    this.selectedIcon,
+    required this.icon,
+  }) : assert(splashRadius == null || splashRadius > 0),
+       _variant = _IconButtonVariant.filled;
+
+  /// Create a filled tonal variant of IconButton.
+  ///
+  /// Filled tonal icon buttons are a middle ground between filled and outlined
+  /// icon buttons. They’re useful in contexts where the button requires slightly
+  /// more emphasis than an outline would give, such as a secondary action paired
+  /// with a high emphasis action.
+  const IconButton.filledTonal({
+    super.key,
+    this.iconSize,
+    this.visualDensity,
+    this.padding,
+    this.alignment,
+    this.splashRadius,
+    this.color,
+    this.focusColor,
+    this.hoverColor,
+    this.highlightColor,
+    this.splashColor,
+    this.disabledColor,
+    required this.onPressed,
+    this.mouseCursor,
+    this.focusNode,
+    this.autofocus = false,
+    this.tooltip,
+    this.enableFeedback,
+    this.constraints,
+    this.style,
+    this.isSelected,
+    this.selectedIcon,
+    required this.icon,
+  }) : assert(splashRadius == null || splashRadius > 0),
+       _variant = _IconButtonVariant.filledTonal;
+
+  /// Create a filled tonal variant of IconButton.
+  ///
+  /// Outlined icon buttons are medium-emphasis buttons. They’re useful when an
+  /// icon button needs more emphasis than a standard icon button but less than
+  /// a filled or filled tonal icon button.
+  const IconButton.outlined({
+    super.key,
+    this.iconSize,
+    this.visualDensity,
+    this.padding,
+    this.alignment,
+    this.splashRadius,
+    this.color,
+    this.focusColor,
+    this.hoverColor,
+    this.highlightColor,
+    this.splashColor,
+    this.disabledColor,
+    required this.onPressed,
+    this.mouseCursor,
+    this.focusNode,
+    this.autofocus = false,
+    this.tooltip,
+    this.enableFeedback,
+    this.constraints,
+    this.style,
+    this.isSelected,
+    this.selectedIcon,
+    required this.icon,
+  }) : assert(splashRadius == null || splashRadius > 0),
+       _variant = _IconButtonVariant.outlined;
 
   /// The size of the icon inside the button.
   ///
@@ -465,6 +565,8 @@
   /// * [ImageIcon], for showing icons from [AssetImage]s or other [ImageProvider]s.
   final Widget? selectedIcon;
 
+  final _IconButtonVariant _variant;
+
   /// A static convenience method that constructs an icon button
   /// [ButtonStyle] given simple values. This method is only used for Material 3.
   ///
@@ -615,6 +717,7 @@
         autofocus: autofocus,
         focusNode: focusNode,
         isSelected: isSelected,
+        variant: _variant,
         child: iconButton,
       );
     }
@@ -714,6 +817,7 @@
     this.isSelected,
     this.style,
     this.focusNode,
+    required this.variant,
     required this.autofocus,
     required this.onPressed,
     required this.child,
@@ -722,6 +826,7 @@
   final bool? isSelected;
   final ButtonStyle? style;
   final FocusNode? focusNode;
+  final _IconButtonVariant variant;
   final bool autofocus;
   final VoidCallback? onPressed;
   final Widget child;
@@ -761,12 +866,16 @@
 
   @override
   Widget build(BuildContext context) {
+    final bool toggleable = widget.isSelected != null;
+
     return _IconButtonM3(
       statesController: statesController,
       style: widget.style,
       autofocus: widget.autofocus,
       focusNode: widget.focusNode,
       onPressed: widget.onPressed,
+      variant: widget.variant,
+      toggleable: toggleable,
       child: widget.child,
     );
   }
@@ -779,6 +888,8 @@
     super.focusNode,
     super.autofocus = false,
     super.statesController,
+    required this.variant,
+    required this.toggleable,
     required Widget super.child,
   }) : super(
       onLongPress: null,
@@ -786,6 +897,9 @@
       onFocusChange: null,
       clipBehavior: Clip.none);
 
+  final _IconButtonVariant variant;
+  final bool toggleable;
+
   /// ## Material 3 defaults
   ///
   /// If [ThemeData.useMaterial3] is set to true the following defaults will
@@ -825,7 +939,16 @@
   /// * `splashFactory` - Theme.splashFactory
   @override
   ButtonStyle defaultStyleOf(BuildContext context) {
-    return _IconButtonDefaultsM3(context);
+    switch (variant) {
+      case _IconButtonVariant.filled:
+        return _FilledIconButtonDefaultsM3(context, toggleable);
+      case _IconButtonVariant.filledTonal:
+        return _FilledTonalIconButtonDefaultsM3(context, toggleable);
+      case _IconButtonVariant.outlined:
+        return _OutlinedIconButtonDefaultsM3(context, toggleable);
+      case _IconButtonVariant.standard:
+        return _IconButtonDefaultsM3(context, toggleable);
+    }
   }
 
   /// Returns the [IconButtonThemeData.style] of the closest [IconButtonTheme] ancestor.
@@ -963,15 +1086,16 @@
 // Token database version: v0_158
 
 class _IconButtonDefaultsM3 extends ButtonStyle {
-  _IconButtonDefaultsM3(this.context)
+  _IconButtonDefaultsM3(this.context, this.toggleable)
     : super(
         animationDuration: kThemeChangeDuration,
         enableFeedback: true,
         alignment: Alignment.center,
       );
 
-    final BuildContext context;
-    late final ColorScheme _colors = Theme.of(context).colorScheme;
+  final BuildContext context;
+  final bool toggleable;
+  late final ColorScheme _colors = Theme.of(context).colorScheme;
 
   // No default text style
 
@@ -1014,7 +1138,7 @@
       if (states.contains(MaterialState.pressed)) {
         return _colors.onSurfaceVariant.withOpacity(0.12);
       }
-      return null;
+      return Colors.transparent;
     });
 
   @override
@@ -1047,7 +1171,8 @@
   MaterialStateProperty<double>? get iconSize =>
     const MaterialStatePropertyAll<double>(24.0);
 
-  // No default side
+  @override
+  MaterialStateProperty<BorderSide?>? get side => null;
 
   @override
   MaterialStateProperty<OutlinedBorder>? get shape =>
@@ -1073,3 +1198,442 @@
 }
 
 // END GENERATED TOKEN PROPERTIES - IconButton
+
+// BEGIN GENERATED TOKEN PROPERTIES - FilledIconButton
+
+// Do not edit by hand. The code between the "BEGIN GENERATED" and
+// "END GENERATED" comments are generated from data in the Material
+// Design token database by the script:
+//   dev/tools/gen_defaults/bin/gen_defaults.dart.
+
+// Token database version: v0_158
+
+class _FilledIconButtonDefaultsM3 extends ButtonStyle {
+  _FilledIconButtonDefaultsM3(this.context, this.toggleable)
+    : super(
+        animationDuration: kThemeChangeDuration,
+        enableFeedback: true,
+        alignment: Alignment.center,
+      );
+
+  final BuildContext context;
+  final bool toggleable;
+  late final ColorScheme _colors = Theme.of(context).colorScheme;
+
+  // No default text style
+
+  @override
+  MaterialStateProperty<Color?>? get backgroundColor =>
+    MaterialStateProperty.resolveWith((Set<MaterialState> states) {
+      if (states.contains(MaterialState.disabled)) {
+        return _colors.onSurface.withOpacity(0.12);
+      }
+      if (states.contains(MaterialState.selected)) {
+        return _colors.primary;
+      }
+      if (toggleable) { // toggleable but unselected case
+        return _colors.surfaceVariant;
+      }
+      return _colors.primary;
+    });
+
+  @override
+  MaterialStateProperty<Color?>? get foregroundColor =>
+    MaterialStateProperty.resolveWith((Set<MaterialState> states) {
+      if (states.contains(MaterialState.disabled)) {
+        return _colors.onSurface.withOpacity(0.38);
+      }
+      if (states.contains(MaterialState.selected)) {
+        return _colors.onPrimary;
+      }
+      if (toggleable) { // toggleable but unselected case
+        return _colors.primary;
+      }
+      return _colors.onPrimary;
+    });
+
+ @override
+  MaterialStateProperty<Color?>? get overlayColor =>
+    MaterialStateProperty.resolveWith((Set<MaterialState> states) {
+      if (states.contains(MaterialState.selected)) {
+        if (states.contains(MaterialState.hovered)) {
+          return _colors.onPrimary.withOpacity(0.08);
+        }
+        if (states.contains(MaterialState.focused)) {
+          return _colors.onPrimary.withOpacity(0.12);
+        }
+        if (states.contains(MaterialState.pressed)) {
+          return _colors.onPrimary.withOpacity(0.12);
+        }
+      }
+      if (toggleable) { // toggleable but unselected case
+        if (states.contains(MaterialState.hovered)) {
+          return _colors.primary.withOpacity(0.08);
+        }
+        if (states.contains(MaterialState.focused)) {
+          return _colors.primary.withOpacity(0.12);
+        }
+        if (states.contains(MaterialState.pressed)) {
+          return _colors.primary.withOpacity(0.12);
+        }
+      }
+      if (states.contains(MaterialState.hovered)) {
+        return _colors.onPrimary.withOpacity(0.08);
+      }
+      if (states.contains(MaterialState.focused)) {
+        return _colors.onPrimary.withOpacity(0.12);
+      }
+      if (states.contains(MaterialState.pressed)) {
+        return _colors.onPrimary.withOpacity(0.12);
+      }
+      return Colors.transparent;
+    });
+
+  @override
+  MaterialStateProperty<double>? get elevation =>
+    const MaterialStatePropertyAll<double>(0.0);
+
+  @override
+  MaterialStateProperty<Color>? get shadowColor =>
+    const MaterialStatePropertyAll<Color>(Colors.transparent);
+
+  @override
+  MaterialStateProperty<Color>? get surfaceTintColor =>
+    const MaterialStatePropertyAll<Color>(Colors.transparent);
+
+  @override
+  MaterialStateProperty<EdgeInsetsGeometry>? get padding =>
+    const MaterialStatePropertyAll<EdgeInsetsGeometry>(EdgeInsets.all(8.0));
+
+  @override
+  MaterialStateProperty<Size>? get minimumSize =>
+    const MaterialStatePropertyAll<Size>(Size(40.0, 40.0));
+
+  // No default fixedSize
+
+  @override
+  MaterialStateProperty<Size>? get maximumSize =>
+    const MaterialStatePropertyAll<Size>(Size.infinite);
+
+  @override
+  MaterialStateProperty<double>? get iconSize =>
+    const MaterialStatePropertyAll<double>(24.0);
+
+  @override
+  MaterialStateProperty<BorderSide?>? get side => null;
+
+  @override
+  MaterialStateProperty<OutlinedBorder>? get shape =>
+    const MaterialStatePropertyAll<OutlinedBorder>(StadiumBorder());
+
+  @override
+  MaterialStateProperty<MouseCursor?>? get mouseCursor =>
+    MaterialStateProperty.resolveWith((Set<MaterialState> states) {
+      if (states.contains(MaterialState.disabled)) {
+        return SystemMouseCursors.basic;
+      }
+      return SystemMouseCursors.click;
+    });
+
+  @override
+  VisualDensity? get visualDensity => VisualDensity.standard;
+
+  @override
+  MaterialTapTargetSize? get tapTargetSize => Theme.of(context).materialTapTargetSize;
+
+  @override
+  InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
+}
+
+// END GENERATED TOKEN PROPERTIES - FilledIconButton
+
+// BEGIN GENERATED TOKEN PROPERTIES - FilledTonalIconButton
+
+// Do not edit by hand. The code between the "BEGIN GENERATED" and
+// "END GENERATED" comments are generated from data in the Material
+// Design token database by the script:
+//   dev/tools/gen_defaults/bin/gen_defaults.dart.
+
+// Token database version: v0_158
+
+class _FilledTonalIconButtonDefaultsM3 extends ButtonStyle {
+  _FilledTonalIconButtonDefaultsM3(this.context, this.toggleable)
+    : super(
+        animationDuration: kThemeChangeDuration,
+        enableFeedback: true,
+        alignment: Alignment.center,
+      );
+
+  final BuildContext context;
+  final bool toggleable;
+  late final ColorScheme _colors = Theme.of(context).colorScheme;
+
+  // No default text style
+
+  @override
+  MaterialStateProperty<Color?>? get backgroundColor =>
+    MaterialStateProperty.resolveWith((Set<MaterialState> states) {
+      if (states.contains(MaterialState.disabled)) {
+        return _colors.onSurface.withOpacity(0.12);
+      }
+      if (states.contains(MaterialState.selected)) {
+        return _colors.secondaryContainer;
+      }
+      if (toggleable) { // toggleable but unselected case
+        return _colors.surfaceVariant;
+      }
+      return _colors.secondaryContainer;
+    });
+
+  @override
+  MaterialStateProperty<Color?>? get foregroundColor =>
+    MaterialStateProperty.resolveWith((Set<MaterialState> states) {
+      if (states.contains(MaterialState.disabled)) {
+        return _colors.onSurface.withOpacity(0.38);
+      }
+      if (states.contains(MaterialState.selected)) {
+        return _colors.onSecondaryContainer;
+      }
+      if (toggleable) { // toggleable but unselected case
+        return _colors.onSurfaceVariant;
+      }
+      return _colors.onSecondaryContainer;
+    });
+
+ @override
+  MaterialStateProperty<Color?>? get overlayColor =>
+    MaterialStateProperty.resolveWith((Set<MaterialState> states) {
+      if (states.contains(MaterialState.selected)) {
+        if (states.contains(MaterialState.hovered)) {
+          return _colors.onSecondaryContainer.withOpacity(0.08);
+        }
+        if (states.contains(MaterialState.focused)) {
+          return _colors.onSecondaryContainer.withOpacity(0.12);
+        }
+        if (states.contains(MaterialState.pressed)) {
+          return _colors.onSecondaryContainer.withOpacity(0.12);
+        }
+      }
+      if (toggleable) { // toggleable but unselected case
+        if (states.contains(MaterialState.hovered)) {
+          return _colors.onSurfaceVariant.withOpacity(0.08);
+        }
+        if (states.contains(MaterialState.focused)) {
+          return _colors.onSurfaceVariant.withOpacity(0.12);
+        }
+        if (states.contains(MaterialState.pressed)) {
+          return _colors.onSurfaceVariant.withOpacity(0.12);
+        }
+      }
+      if (states.contains(MaterialState.hovered)) {
+        return _colors.onSecondaryContainer.withOpacity(0.08);
+      }
+      if (states.contains(MaterialState.focused)) {
+        return _colors.onSecondaryContainer.withOpacity(0.12);
+      }
+      if (states.contains(MaterialState.pressed)) {
+        return _colors.onSecondaryContainer.withOpacity(0.12);
+      }
+      return Colors.transparent;
+    });
+
+  @override
+  MaterialStateProperty<double>? get elevation =>
+    const MaterialStatePropertyAll<double>(0.0);
+
+  @override
+  MaterialStateProperty<Color>? get shadowColor =>
+    const MaterialStatePropertyAll<Color>(Colors.transparent);
+
+  @override
+  MaterialStateProperty<Color>? get surfaceTintColor =>
+    const MaterialStatePropertyAll<Color>(Colors.transparent);
+
+  @override
+  MaterialStateProperty<EdgeInsetsGeometry>? get padding =>
+    const MaterialStatePropertyAll<EdgeInsetsGeometry>(EdgeInsets.all(8.0));
+
+  @override
+  MaterialStateProperty<Size>? get minimumSize =>
+    const MaterialStatePropertyAll<Size>(Size(40.0, 40.0));
+
+  // No default fixedSize
+
+  @override
+  MaterialStateProperty<Size>? get maximumSize =>
+    const MaterialStatePropertyAll<Size>(Size.infinite);
+
+  @override
+  MaterialStateProperty<double>? get iconSize =>
+    const MaterialStatePropertyAll<double>(24.0);
+
+  @override
+  MaterialStateProperty<BorderSide?>? get side => null;
+
+  @override
+  MaterialStateProperty<OutlinedBorder>? get shape =>
+    const MaterialStatePropertyAll<OutlinedBorder>(StadiumBorder());
+
+  @override
+  MaterialStateProperty<MouseCursor?>? get mouseCursor =>
+    MaterialStateProperty.resolveWith((Set<MaterialState> states) {
+      if (states.contains(MaterialState.disabled)) {
+        return SystemMouseCursors.basic;
+      }
+      return SystemMouseCursors.click;
+    });
+
+  @override
+  VisualDensity? get visualDensity => VisualDensity.standard;
+
+  @override
+  MaterialTapTargetSize? get tapTargetSize => Theme.of(context).materialTapTargetSize;
+
+  @override
+  InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
+}
+
+// END GENERATED TOKEN PROPERTIES - FilledTonalIconButton
+
+// BEGIN GENERATED TOKEN PROPERTIES - OutlinedIconButton
+
+// Do not edit by hand. The code between the "BEGIN GENERATED" and
+// "END GENERATED" comments are generated from data in the Material
+// Design token database by the script:
+//   dev/tools/gen_defaults/bin/gen_defaults.dart.
+
+// Token database version: v0_158
+
+class _OutlinedIconButtonDefaultsM3 extends ButtonStyle {
+  _OutlinedIconButtonDefaultsM3(this.context, this.toggleable)
+    : super(
+        animationDuration: kThemeChangeDuration,
+        enableFeedback: true,
+        alignment: Alignment.center,
+      );
+
+  final BuildContext context;
+  final bool toggleable;
+  late final ColorScheme _colors = Theme.of(context).colorScheme;
+
+  // No default text style
+
+  @override
+  MaterialStateProperty<Color?>? get backgroundColor =>
+    MaterialStateProperty.resolveWith((Set<MaterialState> states) {
+      if (states.contains(MaterialState.disabled)) {
+        if (states.contains(MaterialState.selected)) {
+          return _colors.onSurface.withOpacity(0.12);
+        }
+        return Colors.transparent;
+      }
+      if (states.contains(MaterialState.selected)) {
+        return _colors.inverseSurface;
+      }
+      return Colors.transparent;
+    });
+
+  @override
+  MaterialStateProperty<Color?>? get foregroundColor =>
+    MaterialStateProperty.resolveWith((Set<MaterialState> states) {
+      if (states.contains(MaterialState.disabled)) {
+        return _colors.onSurface.withOpacity(0.38);
+      }
+      if (states.contains(MaterialState.selected)) {
+        return _colors.onInverseSurface;
+      }
+      return _colors.onSurfaceVariant;
+    });
+
+ @override
+  MaterialStateProperty<Color?>? get overlayColor =>    MaterialStateProperty.resolveWith((Set<MaterialState> states) {
+      if (states.contains(MaterialState.selected)) {
+        if (states.contains(MaterialState.hovered)) {
+          return _colors.onInverseSurface.withOpacity(0.08);
+        }
+        if (states.contains(MaterialState.focused)) {
+          return _colors.onInverseSurface.withOpacity(0.08);
+        }
+        if (states.contains(MaterialState.pressed)) {
+          return _colors.onInverseSurface.withOpacity(0.12);
+        }
+      }
+      if (states.contains(MaterialState.hovered)) {
+        return _colors.onSurfaceVariant.withOpacity(0.08);
+      }
+      if (states.contains(MaterialState.focused)) {
+        return _colors.onSurfaceVariant.withOpacity(0.08);
+      }
+      if (states.contains(MaterialState.pressed)) {
+        return _colors.onSurface.withOpacity(0.12);
+      }
+      return Colors.transparent;
+    });
+
+  @override
+  MaterialStateProperty<double>? get elevation =>
+    const MaterialStatePropertyAll<double>(0.0);
+
+  @override
+  MaterialStateProperty<Color>? get shadowColor =>
+    const MaterialStatePropertyAll<Color>(Colors.transparent);
+
+  @override
+  MaterialStateProperty<Color>? get surfaceTintColor =>
+    const MaterialStatePropertyAll<Color>(Colors.transparent);
+
+  @override
+  MaterialStateProperty<EdgeInsetsGeometry>? get padding =>
+    const MaterialStatePropertyAll<EdgeInsetsGeometry>(EdgeInsets.all(8.0));
+
+  @override
+  MaterialStateProperty<Size>? get minimumSize =>
+    const MaterialStatePropertyAll<Size>(Size(40.0, 40.0));
+
+  // No default fixedSize
+
+  @override
+  MaterialStateProperty<Size>? get maximumSize =>
+    const MaterialStatePropertyAll<Size>(Size.infinite);
+
+  @override
+  MaterialStateProperty<double>? get iconSize =>
+    const MaterialStatePropertyAll<double>(24.0);
+
+  @override
+  MaterialStateProperty<BorderSide?>? get side =>
+    MaterialStateProperty.resolveWith((Set<MaterialState> states) {
+      if (states.contains(MaterialState.selected)) {
+        return null;
+      } else {
+        if (states.contains(MaterialState.disabled)) {
+          return BorderSide(color: _colors.onSurface.withOpacity(0.12));
+        }
+        return BorderSide(color: _colors.outline);
+      }
+    });
+
+  @override
+  MaterialStateProperty<OutlinedBorder>? get shape =>
+    const MaterialStatePropertyAll<OutlinedBorder>(StadiumBorder());
+
+  @override
+  MaterialStateProperty<MouseCursor?>? get mouseCursor =>
+    MaterialStateProperty.resolveWith((Set<MaterialState> states) {
+      if (states.contains(MaterialState.disabled)) {
+        return SystemMouseCursors.basic;
+      }
+      return SystemMouseCursors.click;
+    });
+
+  @override
+  VisualDensity? get visualDensity => VisualDensity.standard;
+
+  @override
+  MaterialTapTargetSize? get tapTargetSize => Theme.of(context).materialTapTargetSize;
+
+  @override
+  InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
+}
+
+// END GENERATED TOKEN PROPERTIES - OutlinedIconButton
diff --git a/framework/lib/src/material/icon_button_theme.dart b/framework/lib/src/material/icon_button_theme.dart
index 1f5261d..4c8cff4 100644
--- a/framework/lib/src/material/icon_button_theme.dart
+++ b/framework/lib/src/material/icon_button_theme.dart
@@ -49,8 +49,8 @@
 
   /// Linearly interpolate between two icon button themes.
   static IconButtonThemeData? lerp(IconButtonThemeData? a, IconButtonThemeData? b, double t) {
-    if (a == null && b == null) {
-      return null;
+    if (identical(a, b)) {
+      return a;
     }
     return IconButtonThemeData(
       style: ButtonStyle.lerp(a?.style, b?.style, t),
diff --git a/framework/lib/src/material/ink_decoration.dart b/framework/lib/src/material/ink_decoration.dart
index 3d5dd9d..159ae9d 100644
--- a/framework/lib/src/material/ink_decoration.dart
+++ b/framework/lib/src/material/ink_decoration.dart
@@ -288,7 +288,7 @@
       _ink!.decoration = widget.decoration;
       _ink!.configuration = createLocalImageConfiguration(context);
     }
-    return widget.child ?? const SizedBox();
+    return widget.child ?? ConstrainedBox(constraints: const BoxConstraints.expand());
   }
 
   @override
diff --git a/framework/lib/src/material/ink_sparkle.dart b/framework/lib/src/material/ink_sparkle.dart
index 6d3c7cc..3c1b1af 100644
--- a/framework/lib/src/material/ink_sparkle.dart
+++ b/framework/lib/src/material/ink_sparkle.dart
@@ -203,7 +203,7 @@
       ],
     ).animate(_animationController);
 
-    // Creates an element of randomness so that ink eminating from the same
+    // Creates an element of randomness so that ink emanating from the same
     // pixel have slightly different rings and sparkles.
     _turbulenceSeed = turbulenceSeed ?? math.Random().nextDouble() * 1000.0;
   }
diff --git a/framework/lib/src/material/input_decorator.dart b/framework/lib/src/material/input_decorator.dart
index 7b2b408..b6dd321 100644
--- a/framework/lib/src/material/input_decorator.dart
+++ b/framework/lib/src/material/input_decorator.dart
@@ -2741,7 +2741,7 @@
   /// If null, defaults to a value derived from the base [TextStyle] for the
   /// input field and the current [Theme].
   ///
-  /// Note that if you specify this style it will override the default behavior
+  /// Specifying this style will override the default behavior
   /// of [InputDecoration] that changes the color of the label to the
   /// [InputDecoration.errorStyle] color or [ColorScheme.error].
   ///
@@ -2771,7 +2771,7 @@
   ///
   /// If null, defaults to [labelStyle].
   ///
-  /// Note that if you specify this style it will override the default behavior
+  /// Specifying this style will override the default behavior
   /// of [InputDecoration] that changes the color of the label to the
   /// [InputDecoration.errorStyle] color or [ColorScheme.error].
   ///
@@ -2871,8 +2871,8 @@
   /// By default the color of style will be used by the label of
   /// [InputDecoration] if [InputDecoration.errorText] is not null. See
   /// [InputDecoration.labelStyle] or [InputDecoration.floatingLabelStyle] for
-  /// an example of how to replicate this behavior if you have specified either
-  /// style.
+  /// an example of how to replicate this behavior when specifying those
+  /// styles.
   /// {@endtemplate}
   final TextStyle? errorStyle;
 
@@ -3022,7 +3022,7 @@
   /// This example shows the differences between two `TextField` widgets when
   /// [prefixIconConstraints] is set to the default value and when one is not.
   ///
-  /// Note that [isDense] must be set to true to be able to
+  /// The [isDense] property must be set to true to be able to
   /// set the constraints smaller than 48px.
   ///
   /// If null, [BoxConstraints] with a minimum width and height of 48px is
@@ -3199,7 +3199,7 @@
   /// This example shows the differences between two `TextField` widgets when
   /// [suffixIconConstraints] is set to the default value and when one is not.
   ///
-  /// Note that [isDense] must be set to true to be able to
+  /// The [isDense] property must be set to true to be able to
   /// set the constraints smaller than 48px.
   ///
   /// If null, [BoxConstraints] with a minimum width and height of 48px is
@@ -4645,23 +4645,23 @@
     final TextStyle textStyle = _textTheme.bodyLarge ?? const TextStyle();
     if(states.contains(MaterialState.error)) {
       if (states.contains(MaterialState.focused)) {
-        return textStyle.copyWith(color:_colors.error);
+        return textStyle.copyWith(color: _colors.error);
       }
       if (states.contains(MaterialState.hovered)) {
-        return textStyle.copyWith(color:_colors.onErrorContainer);
+        return textStyle.copyWith(color: _colors.onErrorContainer);
       }
-      return textStyle.copyWith(color:_colors.error);
+      return textStyle.copyWith(color: _colors.error);
     }
     if (states.contains(MaterialState.focused)) {
-      return textStyle.copyWith(color:_colors.primary);
+      return textStyle.copyWith(color: _colors.primary);
     }
     if (states.contains(MaterialState.hovered)) {
-      return textStyle.copyWith(color:_colors.onSurfaceVariant);
+      return textStyle.copyWith(color: _colors.onSurfaceVariant);
     }
     if (states.contains(MaterialState.disabled)) {
-      return textStyle.copyWith(color:_colors.onSurface.withOpacity(0.38));
+      return textStyle.copyWith(color: _colors.onSurface.withOpacity(0.38));
     }
-    return textStyle.copyWith(color:_colors.onSurfaceVariant);
+    return textStyle.copyWith(color: _colors.onSurfaceVariant);
   });
 
   @override
@@ -4669,38 +4669,38 @@
     final TextStyle textStyle = _textTheme.bodyLarge ?? const TextStyle();
     if(states.contains(MaterialState.error)) {
       if (states.contains(MaterialState.focused)) {
-        return textStyle.copyWith(color:_colors.error);
+        return textStyle.copyWith(color: _colors.error);
       }
       if (states.contains(MaterialState.hovered)) {
-        return textStyle.copyWith(color:_colors.onErrorContainer);
+        return textStyle.copyWith(color: _colors.onErrorContainer);
       }
-      return textStyle.copyWith(color:_colors.error);
+      return textStyle.copyWith(color: _colors.error);
     }
     if (states.contains(MaterialState.focused)) {
-      return textStyle.copyWith(color:_colors.primary);
+      return textStyle.copyWith(color: _colors.primary);
     }
     if (states.contains(MaterialState.hovered)) {
-      return textStyle.copyWith(color:_colors.onSurfaceVariant);
+      return textStyle.copyWith(color: _colors.onSurfaceVariant);
     }
     if (states.contains(MaterialState.disabled)) {
-      return textStyle.copyWith(color:_colors.onSurface.withOpacity(0.38));
+      return textStyle.copyWith(color: _colors.onSurface.withOpacity(0.38));
     }
-    return textStyle.copyWith(color:_colors.onSurfaceVariant);
+    return textStyle.copyWith(color: _colors.onSurfaceVariant);
   });
 
   @override
   TextStyle? get helperStyle => MaterialStateTextStyle.resolveWith((Set<MaterialState> states) {
     final TextStyle textStyle = _textTheme.bodySmall ?? const TextStyle();
     if (states.contains(MaterialState.disabled)) {
-      return textStyle.copyWith(color:_colors.onSurface.withOpacity(0.38));
+      return textStyle.copyWith(color: _colors.onSurface.withOpacity(0.38));
     }
-    return textStyle.copyWith(color:_colors.onSurfaceVariant);
+    return textStyle.copyWith(color: _colors.onSurfaceVariant);
   });
 
   @override
   TextStyle? get errorStyle => MaterialStateTextStyle.resolveWith((Set<MaterialState> states) {
     final TextStyle textStyle = _textTheme.bodySmall ?? const TextStyle();
-    return textStyle.copyWith(color:_colors.error);
+    return textStyle.copyWith(color: _colors.error);
   });
 }
 
diff --git a/framework/lib/src/material/list_tile.dart b/framework/lib/src/material/list_tile.dart
index 7fa0ea3..642e6a1 100644
--- a/framework/lib/src/material/list_tile.dart
+++ b/framework/lib/src/material/list_tile.dart
@@ -131,7 +131,7 @@
 /// see the example below to see how to adhere to both Material spec and
 /// accessibility requirements.
 ///
-/// Note that [leading] and [trailing] widgets can expand as far as they wish
+/// The [leading] and [trailing] widgets can expand as far as they wish
 /// horizontally, so ensure that they are properly constrained.
 ///
 /// List tiles are typically used in [ListView]s, or arranged in [Column]s in
diff --git a/framework/lib/src/material/list_tile_theme.dart b/framework/lib/src/material/list_tile_theme.dart
index 9be598a..e57ac36 100644
--- a/framework/lib/src/material/list_tile_theme.dart
+++ b/framework/lib/src/material/list_tile_theme.dart
@@ -172,8 +172,8 @@
 
   /// Linearly interpolate between ListTileThemeData objects.
   static ListTileThemeData? lerp(ListTileThemeData? a, ListTileThemeData? b, double t) {
-    if (a == null && b == null) {
-      return null;
+    if (identical(a, b)) {
+      return a;
     }
     return ListTileThemeData(
       dense: t < 0.5 ? a?.dense : b?.dense,
@@ -480,6 +480,9 @@
     Color? selectedColor,
     Color? iconColor,
     Color? textColor,
+    TextStyle? titleTextStyle,
+    TextStyle? subtitleTextStyle,
+    TextStyle? leadingAndTrailingTextStyle,
     EdgeInsetsGeometry? contentPadding,
     Color? tileColor,
     Color? selectedTileColor,
@@ -488,6 +491,8 @@
     double? minVerticalPadding,
     double? minLeadingWidth,
     ListTileTitleAlignment? titleAlignment,
+    MaterialStateProperty<MouseCursor?>? mouseCursor,
+    VisualDensity? visualDensity,
     required Widget child,
   }) {
     return Builder(
@@ -502,6 +507,9 @@
             selectedColor: selectedColor ?? parent.selectedColor,
             iconColor: iconColor ?? parent.iconColor,
             textColor: textColor ?? parent.textColor,
+            titleTextStyle: titleTextStyle ?? parent.titleTextStyle,
+            subtitleTextStyle: subtitleTextStyle ?? parent.subtitleTextStyle,
+            leadingAndTrailingTextStyle: leadingAndTrailingTextStyle ?? parent.leadingAndTrailingTextStyle,
             contentPadding: contentPadding ?? parent.contentPadding,
             tileColor: tileColor ?? parent.tileColor,
             selectedTileColor: selectedTileColor ?? parent.selectedTileColor,
@@ -510,6 +518,8 @@
             minVerticalPadding: minVerticalPadding ?? parent.minVerticalPadding,
             minLeadingWidth: minLeadingWidth ?? parent.minLeadingWidth,
             titleAlignment: titleAlignment ?? parent.titleAlignment,
+            mouseCursor: mouseCursor ?? parent.mouseCursor,
+            visualDensity: visualDensity ?? parent.visualDensity,
           ),
           child: child,
         );
diff --git a/framework/lib/src/material/magnifier.dart b/framework/lib/src/material/magnifier.dart
index 075d40e..8832510 100644
--- a/framework/lib/src/material/magnifier.dart
+++ b/framework/lib/src/material/magnifier.dart
@@ -86,7 +86,7 @@
   // Should _only_ be null on construction. This is because of the animation logic.
   //
   // Animations are added when `last_build_y != current_build_y`. This condition
-  // is true on the inital render, which would mean that the inital
+  // is true on the initial render, which would mean that the initial
   // build would be animated - this is undesired. Thus, this is null for the
   // first frame and the condition becomes `magnifierPosition != null && last_build_y != this_build_y`.
   Offset? _magnifierPosition;
diff --git a/framework/lib/src/material/material.dart b/framework/lib/src/material/material.dart
index 74e7cdc..5a391ee 100644
--- a/framework/lib/src/material/material.dart
+++ b/framework/lib/src/material/material.dart
@@ -641,7 +641,7 @@
   }
 
   void _didChangeLayout() {
-    if (_inkFeatures != null && _inkFeatures!.isNotEmpty) {
+    if (_inkFeatures?.isNotEmpty ?? false) {
       markNeedsPaint();
     }
   }
@@ -651,16 +651,18 @@
 
   @override
   void paint(PaintingContext context, Offset offset) {
-    if (_inkFeatures != null && _inkFeatures!.isNotEmpty) {
+    final List<InkFeature>? inkFeatures = _inkFeatures;
+    if (inkFeatures != null && inkFeatures.isNotEmpty) {
       final Canvas canvas = context.canvas;
       canvas.save();
       canvas.translate(offset.dx, offset.dy);
       canvas.clipRect(Offset.zero & size);
-      for (final InkFeature inkFeature in _inkFeatures!) {
+      for (final InkFeature inkFeature in inkFeatures) {
         inkFeature._paint(canvas);
       }
       canvas.restore();
     }
+    assert(inkFeatures == _inkFeatures);
     super.paint(context, offset);
   }
 }
@@ -740,32 +742,71 @@
     onRemoved?.call();
   }
 
+  // Returns the paint transform that allows `fromRenderObject` to perform paint
+  // in `toRenderObject`'s coordinate space.
+  //
+  // Returns null if either `fromRenderObject` or `toRenderObject` is not in the
+  // same render tree, or either of them is in an offscreen subtree (see
+  // RenderObject.paintsChild).
+  static Matrix4? _getPaintTransform(
+    RenderObject fromRenderObject,
+    RenderObject toRenderObject,
+  ) {
+    // The paths to fromRenderObject and toRenderObject's common ancestor.
+    final List<RenderObject> fromPath = <RenderObject>[fromRenderObject];
+    final List<RenderObject> toPath = <RenderObject>[toRenderObject];
+
+    RenderObject from = fromRenderObject;
+    RenderObject to = toRenderObject;
+
+    while (!identical(from, to)) {
+      final int fromDepth = from.depth;
+      final int toDepth = to.depth;
+
+      if (fromDepth >= toDepth) {
+        final AbstractNode? fromParent = from.parent;
+        // Return early if the 2 render objects are not in the same render tree,
+        // or either of them is offscreen and thus won't get painted.
+        if (fromParent is! RenderObject || !fromParent.paintsChild(from)) {
+          return null;
+        }
+        fromPath.add(fromParent);
+        from = fromParent;
+      }
+
+      if (fromDepth <= toDepth) {
+        final AbstractNode? toParent = to.parent;
+        if (toParent is! RenderObject || !toParent.paintsChild(to)) {
+          return null;
+        }
+        toPath.add(toParent);
+        to = toParent;
+      }
+    }
+    assert(identical(from, to));
+
+    final Matrix4 transform = Matrix4.identity();
+    final Matrix4 inverseTransform = Matrix4.identity();
+
+    for (int index = toPath.length - 1; index > 0; index -= 1) {
+      toPath[index].applyPaintTransform(toPath[index - 1], transform);
+    }
+    for (int index = fromPath.length - 1; index > 0; index -= 1) {
+      fromPath[index].applyPaintTransform(fromPath[index - 1], inverseTransform);
+    }
+
+    final double det = inverseTransform.invert();
+    return det != 0 ? (inverseTransform..multiply(transform)) : null;
+  }
+
   void _paint(Canvas canvas) {
     assert(referenceBox.attached);
     assert(!_debugDisposed);
-    // find the chain of renderers from us to the feature's referenceBox
-    final List<RenderObject> descendants = <RenderObject>[referenceBox];
-    RenderObject node = referenceBox;
-    while (node != _controller) {
-      final RenderObject childNode = node;
-      node = node.parent! as RenderObject;
-      if (!node.paintsChild(childNode)) {
-        // Some node between the reference box and this would skip painting on
-        // the reference box, so bail out early and avoid unnecessary painting.
-        // Some cases where this can happen are the reference box being
-        // offstage, in a fully transparent opacity node, or in a keep alive
-        // bucket.
-        return;
-      }
-      descendants.add(node);
-    }
     // determine the transform that gets our coordinate system to be like theirs
-    final Matrix4 transform = Matrix4.identity();
-    assert(descendants.length >= 2);
-    for (int index = descendants.length - 1; index > 0; index -= 1) {
-      descendants[index].applyPaintTransform(descendants[index - 1], transform);
+    final Matrix4? transform = _getPaintTransform(_controller, referenceBox);
+    if (transform != null) {
+      paintFeature(canvas, transform);
     }
-    paintFeature(canvas, transform);
   }
 
   /// Override this method to paint the ink feature.
diff --git a/framework/lib/src/material/menu_anchor.dart b/framework/lib/src/material/menu_anchor.dart
index d47d624..4ac2a51 100644
--- a/framework/lib/src/material/menu_anchor.dart
+++ b/framework/lib/src/material/menu_anchor.dart
@@ -3619,15 +3619,15 @@
 
   @override
   MaterialStateProperty<EdgeInsetsGeometry?>? get padding {
-    return MaterialStatePropertyAll<EdgeInsetsGeometry>(
+    return const MaterialStatePropertyAll<EdgeInsetsGeometry>(
       EdgeInsetsDirectional.symmetric(
-        horizontal: math.max(
-          _kTopLevelMenuHorizontalMinPadding,
-          2 + Theme.of(context).visualDensity.baseSizeAdjustment.dx,
-        ),
+        horizontal: _kTopLevelMenuHorizontalMinPadding
       ),
     );
   }
+
+  @override
+  VisualDensity get visualDensity => Theme.of(context).visualDensity;
 }
 
 class _MenuButtonDefaultsM3 extends ButtonStyle {
@@ -3764,10 +3764,25 @@
 
   // The horizontal padding number comes from the spec.
   EdgeInsetsGeometry _scaledPadding(BuildContext context) {
+    VisualDensity visualDensity = Theme.of(context).visualDensity;
+    // When horizontal VisualDensity is greater than zero, set it to zero
+    // because the [ButtonStyleButton] has already handle the padding based on the density.
+    // However, the [ButtonStyleButton] doesn't allow the [VisualDensity] adjustment
+    // to reduce the width of the left/right padding, so we need to handle it here if
+    // the density is less than zero, such as on desktop platforms.
+    if (visualDensity.horizontal > 0) {
+      visualDensity = VisualDensity(vertical: visualDensity.vertical);
+    }
     return ButtonStyleButton.scaledPadding(
-      const EdgeInsets.symmetric(horizontal: 12),
-      const EdgeInsets.symmetric(horizontal: 8),
-      const EdgeInsets.symmetric(horizontal: 4),
+      EdgeInsets.symmetric(horizontal: math.max(
+        _kMenuViewPadding,
+        _kLabelItemDefaultSpacing + visualDensity.baseSizeAdjustment.dx,
+      )),
+      EdgeInsets.symmetric(horizontal: math.max(
+        _kMenuViewPadding,
+        8 + visualDensity.baseSizeAdjustment.dx,
+      )),
+      const EdgeInsets.symmetric(horizontal: _kMenuViewPadding),
       MediaQuery.maybeTextScaleFactorOf(context) ?? 1,
     );
   }
@@ -3805,15 +3820,13 @@
 
   @override
   MaterialStateProperty<EdgeInsetsGeometry?>? get padding {
-    return MaterialStatePropertyAll<EdgeInsetsGeometry>(
-      EdgeInsetsDirectional.symmetric(
-        vertical: math.max(
-          _kMenuVerticalMinPadding,
-          2 + Theme.of(context).visualDensity.baseSizeAdjustment.dy,
-        ),
-      ),
+    return const MaterialStatePropertyAll<EdgeInsetsGeometry>(
+      EdgeInsetsDirectional.symmetric(vertical: _kMenuVerticalMinPadding),
     );
   }
+
+  @override
+  VisualDensity get visualDensity => Theme.of(context).visualDensity;
 }
 
 // END GENERATED TOKEN PROPERTIES - Menu
diff --git a/framework/lib/src/material/menu_bar_theme.dart b/framework/lib/src/material/menu_bar_theme.dart
index ffda944..b895b0b 100644
--- a/framework/lib/src/material/menu_bar_theme.dart
+++ b/framework/lib/src/material/menu_bar_theme.dart
@@ -41,8 +41,11 @@
   /// Creates a const set of properties used to configure [MenuTheme].
   const MenuBarThemeData({super.style});
 
-  /// Linearly interpolate between two text button themes.
+  /// Linearly interpolate between two [MenuBar] themes.
   static MenuBarThemeData? lerp(MenuBarThemeData? a, MenuBarThemeData? b, double t) {
+    if (identical(a, b)) {
+      return a;
+    }
     return MenuBarThemeData(style: MenuStyle.lerp(a?.style, b?.style, t));
   }
 }
diff --git a/framework/lib/src/material/menu_button_theme.dart b/framework/lib/src/material/menu_button_theme.dart
index 6eef7a4..0025a29 100644
--- a/framework/lib/src/material/menu_button_theme.dart
+++ b/framework/lib/src/material/menu_button_theme.dart
@@ -59,6 +59,9 @@
 
   /// Linearly interpolate between two menu button themes.
   static MenuButtonThemeData? lerp(MenuButtonThemeData? a, MenuButtonThemeData? b, double t) {
+    if (identical(a, b)) {
+      return a;
+    }
     return MenuButtonThemeData(style: ButtonStyle.lerp(a?.style, b?.style, t));
   }
 
diff --git a/framework/lib/src/material/menu_style.dart b/framework/lib/src/material/menu_style.dart
index fa0dc89..8632920 100644
--- a/framework/lib/src/material/menu_style.dart
+++ b/framework/lib/src/material/menu_style.dart
@@ -304,8 +304,8 @@
 
   /// Linearly interpolate between two [MenuStyle]s.
   static MenuStyle? lerp(MenuStyle? a, MenuStyle? b, double t) {
-    if (a == null && b == null) {
-      return null;
+    if (identical(a, b)) {
+      return a;
     }
     return MenuStyle(
       backgroundColor: MaterialStateProperty.lerp<Color?>(a?.backgroundColor, b?.backgroundColor, t, Color.lerp),
diff --git a/framework/lib/src/material/menu_theme.dart b/framework/lib/src/material/menu_theme.dart
index 685ce48..7f541b1 100644
--- a/framework/lib/src/material/menu_theme.dart
+++ b/framework/lib/src/material/menu_theme.dart
@@ -43,6 +43,9 @@
 
   /// Linearly interpolate between two menu button themes.
   static MenuThemeData? lerp(MenuThemeData? a, MenuThemeData? b, double t) {
+    if (identical(a, b)) {
+      return a;
+    }
     return MenuThemeData(style: MenuStyle.lerp(a?.style, b?.style, t));
   }
 
diff --git a/framework/lib/src/material/navigation_bar.dart b/framework/lib/src/material/navigation_bar.dart
index c9bfa99..bdb20b7 100644
--- a/framework/lib/src/material/navigation_bar.dart
+++ b/framework/lib/src/material/navigation_bar.dart
@@ -169,7 +169,7 @@
   /// is used. Otherwise, [ColorScheme.secondary] with an opacity of 0.24 is used.
   final Color? indicatorColor;
 
-  /// The shape of the selected inidicator.
+  /// The shape of the selected indicator.
   ///
   /// If null, [NavigationBarThemeData.indicatorShape] is used. If that
   /// is also null and [ThemeData.useMaterial3] is true, [StadiumBorder] is used.
diff --git a/framework/lib/src/material/navigation_bar_theme.dart b/framework/lib/src/material/navigation_bar_theme.dart
index 9fc4b8b..c3a79ab 100644
--- a/framework/lib/src/material/navigation_bar_theme.dart
+++ b/framework/lib/src/material/navigation_bar_theme.dart
@@ -125,8 +125,8 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static NavigationBarThemeData? lerp(NavigationBarThemeData? a, NavigationBarThemeData? b, double t) {
-    if (a == null && b == null) {
-      return null;
+    if (identical(a, b)) {
+      return a;
     }
     return NavigationBarThemeData(
       height: lerpDouble(a?.height, b?.height, t),
diff --git a/framework/lib/src/material/navigation_drawer.dart b/framework/lib/src/material/navigation_drawer.dart
index 39c06fb..9583844 100644
--- a/framework/lib/src/material/navigation_drawer.dart
+++ b/framework/lib/src/material/navigation_drawer.dart
@@ -98,7 +98,7 @@
   /// If that is also null, defaults to [ColorScheme.secondaryContainer].
   final Color? indicatorColor;
 
-  /// The shape of the selected inidicator.
+  /// The shape of the selected indicator.
   ///
   /// If this is null, [NavigationDrawerThemeData.indicatorShape] is used.
   /// If that is also null, defaults to [StadiumBorder].
@@ -114,7 +114,7 @@
   /// [NavigationDrawerDestination] or null if no destination is selected.
   ///
   /// A valid [selectedIndex] satisfies 0 <= [selectedIndex] < number of [NavigationDrawerDestination].
-  /// For an invalid [selectedIndex] like `-1`, all desitinations will appear unselected.
+  /// For an invalid [selectedIndex] like `-1`, all destinations will appear unselected.
   final int? selectedIndex;
 
   /// Called when one of the [NavigationDrawerDestination] children is selected.
diff --git a/framework/lib/src/material/navigation_drawer_theme.dart b/framework/lib/src/material/navigation_drawer_theme.dart
index 1153a47..e611ce0 100644
--- a/framework/lib/src/material/navigation_drawer_theme.dart
+++ b/framework/lib/src/material/navigation_drawer_theme.dart
@@ -124,10 +124,9 @@
   /// If both arguments are null then null is returned.
   ///
   /// {@macro dart.ui.shadow.lerp}
-  static NavigationDrawerThemeData? lerp(
-      NavigationDrawerThemeData? a, NavigationDrawerThemeData? b, double t) {
-    if (a == null && b == null) {
-      return null;
+  static NavigationDrawerThemeData? lerp(NavigationDrawerThemeData? a, NavigationDrawerThemeData? b, double t) {
+    if (identical(a, b)) {
+      return a;
     }
     return NavigationDrawerThemeData(
       tileHeight: lerpDouble(a?.tileHeight, b?.tileHeight, t),
diff --git a/framework/lib/src/material/navigation_rail.dart b/framework/lib/src/material/navigation_rail.dart
index 27d244c..47b7a7a 100644
--- a/framework/lib/src/material/navigation_rail.dart
+++ b/framework/lib/src/material/navigation_rail.dart
@@ -578,7 +578,7 @@
     );
 
     final bool material3 = Theme.of(context).useMaterial3;
-    final EdgeInsets destionationPadding = (padding ?? EdgeInsets.zero).resolve(Directionality.of(context));
+    final EdgeInsets destinationPadding = (padding ?? EdgeInsets.zero).resolve(Directionality.of(context));
     Offset indicatorOffset;
 
     final Widget themedIcon = IconTheme(
@@ -597,8 +597,8 @@
         // Split the destination spacing across the top and bottom to keep the icon centered.
         final Widget? spacing = material3 ? const SizedBox(height: _verticalDestinationSpacingM3 / 2) : null;
         indicatorOffset = Offset(
-          minWidth / 2 + destionationPadding.left,
-          _verticalDestinationSpacingM3 / 2 + destionationPadding.top,
+          minWidth / 2 + destinationPadding.left,
+          _verticalDestinationSpacingM3 / 2 + destinationPadding.top,
         );
         final Widget iconPart = Column(
           children: <Widget>[
@@ -675,8 +675,8 @@
         final Widget topSpacing = SizedBox(height: material3 ? 0 : verticalPadding);
         final Widget labelSpacing = SizedBox(height: material3 ? lerpDouble(0, _verticalIconLabelSpacingM3, appearingAnimationValue)! : 0);
         final Widget bottomSpacing = SizedBox(height: material3 ? _verticalDestinationSpacingM3 : verticalPadding);
-        final double indicatorHorizontalPadding = (destionationPadding.left / 2) - (destionationPadding.right / 2);
-        final double indicatorVerticalPadding = destionationPadding.top;
+        final double indicatorHorizontalPadding = (destinationPadding.left / 2) - (destinationPadding.right / 2);
+        final double indicatorVerticalPadding = destinationPadding.top;
         indicatorOffset = Offset(minWidth / 2 + indicatorHorizontalPadding, indicatorVerticalPadding);
         if (minWidth < _NavigationRailDefaultsM2(context).minWidth!) {
           indicatorOffset = Offset(minWidth / 2 + _horizontalDestinationSpacingM3, indicatorVerticalPadding);
@@ -723,8 +723,8 @@
         final Widget topSpacing = SizedBox(height: material3 ? 0 : _verticalDestinationPaddingWithLabel);
         final Widget labelSpacing = SizedBox(height: material3 ? _verticalIconLabelSpacingM3 : 0);
         final Widget bottomSpacing = SizedBox(height: material3 ? _verticalDestinationSpacingM3 : _verticalDestinationPaddingWithLabel);
-        final double indicatorHorizontalPadding = (destionationPadding.left / 2) - (destionationPadding.right / 2);
-        final double indicatorVerticalPadding = destionationPadding.top;
+        final double indicatorHorizontalPadding = (destinationPadding.left / 2) - (destinationPadding.right / 2);
+        final double indicatorVerticalPadding = destinationPadding.top;
         indicatorOffset = Offset(minWidth / 2 + indicatorHorizontalPadding, indicatorVerticalPadding);
         if (minWidth < _NavigationRailDefaultsM2(context).minWidth!) {
           indicatorOffset = Offset(minWidth / 2 + _horizontalDestinationSpacingM3, indicatorVerticalPadding);
@@ -944,7 +944,7 @@
   /// The color of the [indicatorShape] when this destination is selected.
   final Color? indicatorColor;
 
-  /// The shape of the selection inidicator.
+  /// The shape of the selection indicator.
   final ShapeBorder? indicatorShape;
 
   /// The label for the destination.
diff --git a/framework/lib/src/material/navigation_rail_theme.dart b/framework/lib/src/material/navigation_rail_theme.dart
index 2253054..cfa023b 100644
--- a/framework/lib/src/material/navigation_rail_theme.dart
+++ b/framework/lib/src/material/navigation_rail_theme.dart
@@ -143,8 +143,8 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static NavigationRailThemeData? lerp(NavigationRailThemeData? a, NavigationRailThemeData? b, double t) {
-    if (a == null && b == null) {
-      return null;
+    if (identical(a, b)) {
+      return a;
     }
     return NavigationRailThemeData(
       backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t),
diff --git a/framework/lib/src/material/outlined_button.dart b/framework/lib/src/material/outlined_button.dart
index 0f26afe..3289c70 100644
--- a/framework/lib/src/material/outlined_button.dart
+++ b/framework/lib/src/material/outlined_button.dart
@@ -287,10 +287,10 @@
   /// * `surfaceTintColor` - null
   /// * `elevation` - 0
   /// * `padding`
-  ///   * `textScaleFactor <= 1` - horizontal(16)
-  ///   * `1 < textScaleFactor <= 2` - lerp(horizontal(16), horizontal(8))
-  ///   * `2 < textScaleFactor <= 3` - lerp(horizontal(8), horizontal(4))
-  ///   * `3 < textScaleFactor` - horizontal(4)
+  ///   * `textScaleFactor <= 1` - horizontal(24)
+  ///   * `1 < textScaleFactor <= 2` - lerp(horizontal(24), horizontal(12))
+  ///   * `2 < textScaleFactor <= 3` - lerp(horizontal(12), horizontal(6))
+  ///   * `3 < textScaleFactor` - horizontal(6)
   /// * `minimumSize` - Size(64, 40)
   /// * `fixedSize` - null
   /// * `maximumSize` - Size.infinite
@@ -307,6 +307,9 @@
   /// * `enableFeedback` - true
   /// * `alignment` - Alignment.center
   /// * `splashFactory` - Theme.splashFactory
+  ///
+  /// For the [OutlinedButton.icon] factory, the start (generally the left) value of
+  /// [padding] is reduced from 24 to 16.
   @override
   ButtonStyle defaultStyleOf(BuildContext context) {
     final ThemeData theme = Theme.of(context);
@@ -347,10 +350,12 @@
 }
 
 EdgeInsetsGeometry _scaledPadding(BuildContext context) {
+  final bool useMaterial3 = Theme.of(context).useMaterial3;
+  final double padding1x = useMaterial3 ? 24.0 : 16.0;
   return ButtonStyleButton.scaledPadding(
-    const EdgeInsets.symmetric(horizontal: 16),
-    const EdgeInsets.symmetric(horizontal: 8),
-    const EdgeInsets.symmetric(horizontal: 4),
+     EdgeInsets.symmetric(horizontal: padding1x),
+     EdgeInsets.symmetric(horizontal: padding1x / 2),
+     EdgeInsets.symmetric(horizontal: padding1x / 2 / 2),
     MediaQuery.textScaleFactorOf(context),
   );
 }
@@ -422,6 +427,23 @@
          clipBehavior: clipBehavior ?? Clip.none,
          child: _OutlinedButtonWithIconChild(icon: icon, label: label),
       );
+
+  @override
+  ButtonStyle defaultStyleOf(BuildContext context) {
+    final bool useMaterial3 = Theme.of(context).useMaterial3;
+    if (!useMaterial3) {
+      return super.defaultStyleOf(context);
+    }
+    final EdgeInsetsGeometry scaledPadding = ButtonStyleButton.scaledPadding(
+      const EdgeInsetsDirectional.fromSTEB(16, 0, 24, 0),
+      const EdgeInsetsDirectional.fromSTEB(8, 0, 12, 0),
+      const EdgeInsetsDirectional.fromSTEB(4, 0, 6, 0),
+      MediaQuery.textScaleFactorOf(context),
+    );
+    return super.defaultStyleOf(context).copyWith(
+      padding: MaterialStatePropertyAll<EdgeInsetsGeometry>(scaledPadding),
+    );
+  }
 }
 
 class _OutlinedButtonWithIconChild extends StatelessWidget {
diff --git a/framework/lib/src/material/outlined_button_theme.dart b/framework/lib/src/material/outlined_button_theme.dart
index bd12404..fd78642 100644
--- a/framework/lib/src/material/outlined_button_theme.dart
+++ b/framework/lib/src/material/outlined_button_theme.dart
@@ -49,8 +49,8 @@
 
   /// Linearly interpolate between two outlined button themes.
   static OutlinedButtonThemeData? lerp(OutlinedButtonThemeData? a, OutlinedButtonThemeData? b, double t) {
-    if (a == null && b == null) {
-      return null;
+    if (identical(a, b)) {
+      return a;
     }
     return OutlinedButtonThemeData(
       style: ButtonStyle.lerp(a?.style, b?.style, t),
diff --git a/framework/lib/src/material/page_transitions_theme.dart b/framework/lib/src/material/page_transitions_theme.dart
index f41fbc7..654cb1b 100644
--- a/framework/lib/src/material/page_transitions_theme.dart
+++ b/framework/lib/src/material/page_transitions_theme.dart
@@ -151,7 +151,7 @@
 class _ZoomPageTransition extends StatelessWidget {
   /// Creates a [_ZoomPageTransition].
   ///
-  /// The [animation] and [secondaryAnimation] argument are required and must
+  /// The [animation] and [secondaryAnimation] arguments are required and must
   /// not be null.
   const _ZoomPageTransition({
     required this.animation,
@@ -196,9 +196,15 @@
 
   /// Whether the [SnapshotWidget] will be used.
   ///
-  /// Notably, this improves performance by disabling animations on both the outgoing and
-  /// incoming route. This also implies that ink-splashes or similar animations will
-  /// not animate during the transition.
+  /// When this value is true, performance is improved by disabling animations
+  /// on both the outgoing and incoming route. This also implies that ink-splashes
+  /// or similar animations will not animate during the transition.
+  ///
+  /// See also:
+  ///
+  ///  * [TransitionRoute.allowSnapshotting], which defines wether the route
+  ///    transition will prefer to animate a snapshot of the entering and exiting
+  ///    routes.
   final bool allowSnapshotting;
 
   /// The widget below this widget in the tree.
@@ -604,9 +610,36 @@
   /// Constructs a page transition animation that matches the transition used on
   /// Android Q.
   const ZoomPageTransitionsBuilder({
+    this.allowSnapshotting = true,
     this.allowEnterRouteSnapshotting = true,
   });
 
+  /// Whether zoom page transitions will prefer to animate a snapshot of the entering
+  /// and exiting routes.
+  ///
+  /// If not specified, defaults to true.
+  ///
+  /// When this value is true, zoom page transitions will snapshot the entering and
+  /// exiting routes. These snapshots are then animated in place of the underlying
+  /// widgets to improve performance of the transition.
+  ///
+  /// Generally this means that animations that occur on the entering/exiting route
+  /// while the route animation plays may appear frozen - unless they are a hero
+  /// animation or something that is drawn in a separate overlay.
+  ///
+  /// {@tool dartpad}
+  /// This example shows a [MaterialApp] that disables snapshotting for the zoom
+  /// transitions on Android.
+  ///
+  /// ** See code in examples/api/lib/material/page_transitions_theme/page_transitions_theme.1.dart **
+  /// {@end-tool}
+  ///
+  /// See also:
+  ///
+  ///  * [PageRoute.allowSnapshotting], which enables or disables snapshotting
+  ///    on a per route basis.
+  final bool allowSnapshotting;
+
   /// Whether to enable snapshotting on the entering route during the
   /// transition animation.
   ///
@@ -627,7 +660,7 @@
     return _ZoomPageTransition(
       animation: animation,
       secondaryAnimation: secondaryAnimation,
-      allowSnapshotting: route?.allowSnapshotting ?? true,
+      allowSnapshotting: allowSnapshotting && (route?.allowSnapshotting ?? true),
       allowEnterRouteSnapshotting: allowEnterRouteSnapshotting,
       child: child,
     );
@@ -669,7 +702,13 @@
 /// and delegates to [buildTransitions].
 ///
 /// If a builder with a matching platform is not found, then the
-/// [FadeUpwardsPageTransitionsBuilder] is used.
+/// [ZoomPageTransitionsBuilder] is used.
+///
+/// {@tool dartpad}
+/// This example shows a [MaterialApp] that defines a custom [PageTransitionsTheme].
+///
+/// ** See code in examples/api/lib/material/page_transitions_theme/page_transitions_theme.0.dart **
+/// {@end-tool}
 ///
 /// See also:
 ///
@@ -702,8 +741,9 @@
   Map<TargetPlatform, PageTransitionsBuilder> get builders => _builders;
   final Map<TargetPlatform, PageTransitionsBuilder> _builders;
 
-  /// Delegates to the builder for the current [ThemeData.platform]
-  /// or [ZoomPageTransitionsBuilder].
+  /// Delegates to the builder for the current [ThemeData.platform].
+  /// If a builder for the current platform is not found, then the
+  /// [ZoomPageTransitionsBuilder] is used.
   ///
   /// [MaterialPageRoute.buildTransitions] delegates to this method.
   Widget buildTransitions<T>(
@@ -724,8 +764,8 @@
     return matchingBuilder.buildTransitions<T>(route, context, animation, secondaryAnimation, child);
   }
 
-  // Just used to the builders Map to a list with one PageTransitionsBuilder per platform
-  // for the operator == overload.
+  // Map the builders to a list with one PageTransitionsBuilder per platform for
+  // the operator == overload.
   List<PageTransitionsBuilder?> _all(Map<TargetPlatform, PageTransitionsBuilder> builders) {
     return TargetPlatform.values.map((TargetPlatform platform) => builders[platform]).toList();
   }
@@ -968,7 +1008,9 @@
 
   @override
   bool shouldRepaint(covariant _ZoomExitTransitionPainter oldDelegate) {
-    return oldDelegate.reverse != reverse || oldDelegate.fade.value != fade.value || oldDelegate.scale.value != scale.value;
+    return oldDelegate.reverse != reverse
+      || oldDelegate.fade.value != fade.value
+      || oldDelegate.scale.value != scale.value;
   }
 
   @override
diff --git a/framework/lib/src/material/paginated_data_table.dart b/framework/lib/src/material/paginated_data_table.dart
index 56ed3b4..8a10251 100644
--- a/framework/lib/src/material/paginated_data_table.dart
+++ b/framework/lib/src/material/paginated_data_table.dart
@@ -71,7 +71,13 @@
     this.sortColumnIndex,
     this.sortAscending = true,
     this.onSelectAll,
-    this.dataRowHeight = kMinInteractiveDimension,
+    @Deprecated(
+      'Migrate to use dataRowMinHeight and dataRowMaxHeight instead. '
+      'This feature was deprecated after v3.7.0-5.0.pre.',
+    )
+    double? dataRowHeight,
+    double? dataRowMinHeight,
+    double? dataRowMaxHeight,
     this.headingRowHeight = 56.0,
     this.horizontalMargin = 24.0,
     this.columnSpacing = 56.0,
@@ -91,6 +97,11 @@
   }) : assert(actions == null || (header != null)),
        assert(columns.isNotEmpty),
        assert(sortColumnIndex == null || (sortColumnIndex >= 0 && sortColumnIndex < columns.length)),
+       assert(dataRowMinHeight == null || dataRowMaxHeight == null || dataRowMaxHeight >= dataRowMinHeight),
+       assert(dataRowHeight == null || (dataRowMinHeight == null && dataRowMaxHeight == null),
+         'dataRowHeight ($dataRowHeight) must not be set if dataRowMinHeight ($dataRowMinHeight) or dataRowMaxHeight ($dataRowMaxHeight) are set.'),
+       dataRowMinHeight = dataRowHeight ?? dataRowMinHeight ?? kMinInteractiveDimension,
+       dataRowMaxHeight = dataRowHeight ?? dataRowMaxHeight ?? kMinInteractiveDimension,
        assert(rowsPerPage > 0),
        assert(() {
          if (onRowsPerPageChanged != null) {
@@ -147,7 +158,23 @@
   ///
   /// This value is optional and defaults to kMinInteractiveDimension if not
   /// specified.
-  final double dataRowHeight;
+  @Deprecated(
+    'Migrate to use dataRowMinHeight and dataRowMaxHeight instead. '
+    'This feature was deprecated after v3.7.0-5.0.pre.',
+  )
+  double? get dataRowHeight => dataRowMinHeight == dataRowMaxHeight ? dataRowMinHeight : null;
+
+  /// The minimum height of each row (excluding the row that contains column headings).
+  ///
+  /// This value is optional and defaults to [kMinInteractiveDimension] if not
+  /// specified.
+  final double dataRowMinHeight;
+
+  /// The maximum height of each row (excluding the row that contains column headings).
+  ///
+  /// This value is optional and defaults to kMinInteractiveDimension if not
+  /// specified.
+  final double dataRowMaxHeight;
 
   /// The height of the heading row.
   ///
@@ -518,7 +545,8 @@
                     // Make sure no decoration is set on the DataTable
                     // from the theme, as its already wrapped in a Card.
                     decoration: const BoxDecoration(),
-                    dataRowHeight: widget.dataRowHeight,
+                    dataRowMinHeight: widget.dataRowMinHeight,
+                    dataRowMaxHeight: widget.dataRowMaxHeight,
                     headingRowHeight: widget.headingRowHeight,
                     horizontalMargin: widget.horizontalMargin,
                     checkboxHorizontalMargin: widget.checkboxHorizontalMargin,
diff --git a/framework/lib/src/material/popup_menu.dart b/framework/lib/src/material/popup_menu.dart
index 44991de..65d89ab 100644
--- a/framework/lib/src/material/popup_menu.dart
+++ b/framework/lib/src/material/popup_menu.dart
@@ -251,7 +251,7 @@
 
   /// The padding of the menu item.
   ///
-  /// Note that [height] may interact with the applied padding. For example,
+  /// The [height] property may interact with the applied padding. For example,
   /// If a [height] greater than the height of the sum of the padding and [child]
   /// is provided, then the padding's effect will not be visible.
   ///
diff --git a/framework/lib/src/material/popup_menu_theme.dart b/framework/lib/src/material/popup_menu_theme.dart
index 9b3d4c8..00314ca 100644
--- a/framework/lib/src/material/popup_menu_theme.dart
+++ b/framework/lib/src/material/popup_menu_theme.dart
@@ -129,8 +129,8 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static PopupMenuThemeData? lerp(PopupMenuThemeData? a, PopupMenuThemeData? b, double t) {
-    if (a == null && b == null) {
-      return null;
+    if (identical(a, b)) {
+      return a;
     }
     return PopupMenuThemeData(
       color: Color.lerp(a?.color, b?.color, t),
diff --git a/framework/lib/src/material/progress_indicator_theme.dart b/framework/lib/src/material/progress_indicator_theme.dart
index b2b5521..6690dee 100644
--- a/framework/lib/src/material/progress_indicator_theme.dart
+++ b/framework/lib/src/material/progress_indicator_theme.dart
@@ -84,8 +84,8 @@
   ///
   /// If both arguments are null, then null is returned.
   static ProgressIndicatorThemeData? lerp(ProgressIndicatorThemeData? a, ProgressIndicatorThemeData? b, double t) {
-    if (a == null && b == null) {
-      return null;
+    if (identical(a, b)) {
+      return a;
     }
     return ProgressIndicatorThemeData(
       color: Color.lerp(a?.color, b?.color, t),
diff --git a/framework/lib/src/material/radio_theme.dart b/framework/lib/src/material/radio_theme.dart
index a3775cc..e45553b 100644
--- a/framework/lib/src/material/radio_theme.dart
+++ b/framework/lib/src/material/radio_theme.dart
@@ -112,6 +112,9 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static RadioThemeData lerp(RadioThemeData? a, RadioThemeData? b, double t) {
+    if (identical(a, b) && a != null) {
+      return a;
+    }
     return RadioThemeData(
       mouseCursor: t < 0.5 ? a?.mouseCursor : b?.mouseCursor,
       fillColor: MaterialStateProperty.lerp<Color?>(a?.fillColor, b?.fillColor, t, Color.lerp),
diff --git a/framework/lib/src/material/refresh_indicator.dart b/framework/lib/src/material/refresh_indicator.dart
index c04a70f..5c217c3 100644
--- a/framework/lib/src/material/refresh_indicator.dart
+++ b/framework/lib/src/material/refresh_indicator.dart
@@ -5,8 +5,8 @@
 import 'dart:async';
 import 'dart:math' as math;
 
+import 'package:flute/cupertino.dart';
 import 'package:flute/foundation.dart' show clampDouble;
-import 'package:flute/widgets.dart';
 
 import 'debug.dart';
 import 'material_localizations.dart';
@@ -59,6 +59,8 @@
   onEdge,
 }
 
+enum _IndicatorType { material, adaptive }
+
 /// A widget that supports the Material "swipe to refresh" idiom.
 ///
 /// {@youtube 560 315 https://www.youtube.com/watch?v=ORApMlzwMdM}
@@ -138,7 +140,38 @@
     this.semanticsValue,
     this.strokeWidth = RefreshProgressIndicator.defaultStrokeWidth,
     this.triggerMode = RefreshIndicatorTriggerMode.onEdge,
-  });
+  }) : _indicatorType = _IndicatorType.material;
+
+  /// Creates an adaptive [RefreshIndicator] based on whether the target
+  /// platform is iOS or macOS, following Material design's
+  /// [Cross-platform guidelines](https://material.io/design/platform-guidance/cross-platform-adaptation.html).
+  ///
+  /// When the descendant overscrolls, a different spinning progress indicator
+  /// is shown depending on platform. On iOS and macOS,
+  /// [CupertinoActivityIndicator] is shown, but on all other platforms,
+  /// [CircularProgressIndicator] appears.
+  ///
+  /// If a [CupertinoActivityIndicator] is shown, the following parameters are ignored:
+  /// [backgroundColor], [semanticsLabel], [semanticsValue], [strokeWidth].
+  ///
+  /// The target platform is based on the current [Theme]: [ThemeData.platform].
+  ///
+  /// Noteably the scrollable widget itself will have slightly different behavior
+  /// from [CupertinoSliverRefreshControl], due to a difference in structure.
+  const RefreshIndicator.adaptive({
+    super.key,
+    required this.child,
+    this.displacement = 40.0,
+    this.edgeOffset = 0.0,
+    required this.onRefresh,
+    this.color,
+    this.backgroundColor,
+    this.notificationPredicate = defaultScrollNotificationPredicate,
+    this.semanticsLabel,
+    this.semanticsValue,
+    this.strokeWidth = RefreshProgressIndicator.defaultStrokeWidth,
+    this.triggerMode = RefreshIndicatorTriggerMode.onEdge,
+  }) : _indicatorType = _IndicatorType.adaptive;
 
   /// The widget below this widget in the tree.
   ///
@@ -207,6 +240,8 @@
   /// By default, the value of [strokeWidth] is 2.0 pixels.
   final double strokeWidth;
 
+  final _IndicatorType _indicatorType;
+
   /// Defines how this [RefreshIndicator] can be triggered when users overscroll.
   ///
   /// The [RefreshIndicator] can be pulled out in two cases,
@@ -555,7 +590,7 @@
                 child: AnimatedBuilder(
                   animation: _positionController,
                   builder: (BuildContext context, Widget? child) {
-                    return RefreshProgressIndicator(
+                    final Widget materialIndicator = RefreshProgressIndicator(
                       semanticsLabel: widget.semanticsLabel ?? MaterialLocalizations.of(context).refreshIndicatorSemanticLabel,
                       semanticsValue: widget.semanticsValue,
                       value: showIndeterminateIndicator ? null : _value.value,
@@ -563,6 +598,29 @@
                       backgroundColor: widget.backgroundColor,
                       strokeWidth: widget.strokeWidth,
                     );
+
+                    final Widget cupertinoIndicator = CupertinoActivityIndicator(
+                      color: widget.color,
+                    );
+
+                    switch(widget._indicatorType) {
+                      case _IndicatorType.material:
+                        return materialIndicator;
+
+                      case _IndicatorType.adaptive: {
+                        final ThemeData theme = Theme.of(context);
+                        switch (theme.platform) {
+                          case TargetPlatform.android:
+                          case TargetPlatform.fuchsia:
+                          case TargetPlatform.linux:
+                          case TargetPlatform.windows:
+                            return materialIndicator;
+                          case TargetPlatform.iOS:
+                          case TargetPlatform.macOS:
+                            return cupertinoIndicator;
+                        }
+                      }
+                    }
                   },
                 ),
               ),
diff --git a/framework/lib/src/material/scaffold.dart b/framework/lib/src/material/scaffold.dart
index a5a0885..06d794e 100644
--- a/framework/lib/src/material/scaffold.dart
+++ b/framework/lib/src/material/scaffold.dart
@@ -1158,9 +1158,9 @@
       positionChild(_ScaffoldSlot.snackBar, Offset(xOffset, snackBarYOffsetBase - snackBarSize.height));
 
       assert((){
-        // Whether a floating SnackBar has been offsetted too high.
+        // Whether a floating SnackBar has been offset too high.
         //
-        // To improve the developper experience, this assert is done after the call to positionChild.
+        // To improve the developer experience, this assert is done after the call to positionChild.
         // if we assert sooner the SnackBar is visible because its defaults position is (0,0) and
         // it can cause confusion to the user as the error message states that the SnackBar is off screen.
         if (isSnackBarFloating) {
diff --git a/framework/lib/src/material/scrollbar_theme.dart b/framework/lib/src/material/scrollbar_theme.dart
index 2257055..3575f5b 100644
--- a/framework/lib/src/material/scrollbar_theme.dart
+++ b/framework/lib/src/material/scrollbar_theme.dart
@@ -204,6 +204,9 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static ScrollbarThemeData lerp(ScrollbarThemeData? a, ScrollbarThemeData? b, double t) {
+    if (identical(a, b) && a != null) {
+      return a;
+    }
     return ScrollbarThemeData(
       thumbVisibility: MaterialStateProperty.lerp<bool?>(a?.thumbVisibility, b?.thumbVisibility, t, _lerpBool),
       thickness: MaterialStateProperty.lerp<double?>(a?.thickness, b?.thickness, t, lerpDouble),
@@ -296,7 +299,7 @@
 ///
 ///  * [ScrollbarThemeData], which describes the configuration of a
 ///    scrollbar theme.
-class ScrollbarTheme extends InheritedWidget {
+class ScrollbarTheme extends InheritedTheme {
   /// Constructs a scrollbar theme that configures all descendant [Scrollbar]
   /// widgets.
   const ScrollbarTheme({
@@ -322,5 +325,10 @@
   }
 
   @override
+  Widget wrap(BuildContext context, Widget child) {
+    return ScrollbarTheme(data: data, child: child);
+  }
+
+  @override
   bool updateShouldNotify(ScrollbarTheme oldWidget) => data != oldWidget.data;
 }
diff --git a/framework/lib/src/material/segmented_button.dart b/framework/lib/src/material/segmented_button.dart
index 23e9706..1234491 100644
--- a/framework/lib/src/material/segmented_button.dart
+++ b/framework/lib/src/material/segmented_button.dart
@@ -100,7 +100,7 @@
   /// more than five options, consider using [FilterChip] or [ChoiceChip]
   /// widgets.
   ///
-  /// If [onSelectionChanged] is null, then the entire segemented button will
+  /// If [onSelectionChanged] is null, then the entire segmented button will
   /// be disabled.
   ///
   /// By default [selected] must only contain one entry. However, if
@@ -163,7 +163,7 @@
 
   /// Determines if having no selected segments is allowed.
   ///
-  /// If true, then it is acceptable for none of the segements to be selected.
+  /// If true, then it is acceptable for none of the segments to be selected.
   /// This means that [selected] can be empty. If the user taps on a
   /// selected segment, it will be removed from the selection set passed into
   /// [onSelectionChanged].
@@ -183,7 +183,7 @@
   ///     dividers between segments.
   ///   * [ButtonStyle.shape]
   ///
-  /// The following style properties are applied to each of the invidual
+  /// The following style properties are applied to each of the individual
   /// button segments. For properties that are a [MaterialStateProperty],
   /// they will be resolved with the current state of the segment:
   ///
diff --git a/framework/lib/src/material/segmented_button_theme.dart b/framework/lib/src/material/segmented_button_theme.dart
index 686455b..f8ec588 100644
--- a/framework/lib/src/material/segmented_button_theme.dart
+++ b/framework/lib/src/material/segmented_button_theme.dart
@@ -62,6 +62,9 @@
 
   /// Linearly interpolates between two segmented button themes.
   static SegmentedButtonThemeData lerp(SegmentedButtonThemeData? a, SegmentedButtonThemeData? b, double t) {
+    if (identical(a, b) && a != null) {
+      return a;
+    }
     return SegmentedButtonThemeData(
       style: ButtonStyle.lerp(a?.style, b?.style, t),
       selectedIcon: t < 0.5 ? a?.selectedIcon : b?.selectedIcon,
diff --git a/framework/lib/src/material/slider_theme.dart b/framework/lib/src/material/slider_theme.dart
index c01c918..7397b30 100644
--- a/framework/lib/src/material/slider_theme.dart
+++ b/framework/lib/src/material/slider_theme.dart
@@ -650,6 +650,9 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static SliderThemeData lerp(SliderThemeData a, SliderThemeData b, double t) {
+    if (identical(a, b)) {
+      return a;
+    }
     return SliderThemeData(
       trackHeight: lerpDouble(a.trackHeight, b.trackHeight, t),
       activeTrackColor: Color.lerp(a.activeTrackColor, b.activeTrackColor, t),
diff --git a/framework/lib/src/material/snack_bar.dart b/framework/lib/src/material/snack_bar.dart
index 67cf2bd..5fcfc52 100644
--- a/framework/lib/src/material/snack_bar.dart
+++ b/framework/lib/src/material/snack_bar.dart
@@ -516,7 +516,7 @@
     // the surrounding theme.
     final Brightness brightness = isThemeDark ? Brightness.light : Brightness.dark;
 
-    // Invert the theme values for Material 2. Material 3 values are tokenzied to pre-inverted values.
+    // Invert the theme values for Material 2. Material 3 values are tokenized to pre-inverted values.
     final ThemeData effectiveTheme = theme.useMaterial3
         ? theme
         : theme.copyWith(
diff --git a/framework/lib/src/material/snack_bar_theme.dart b/framework/lib/src/material/snack_bar_theme.dart
index 820e35a..b7724d6 100644
--- a/framework/lib/src/material/snack_bar_theme.dart
+++ b/framework/lib/src/material/snack_bar_theme.dart
@@ -178,6 +178,9 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static SnackBarThemeData lerp(SnackBarThemeData? a, SnackBarThemeData? b, double t) {
+    if (identical(a, b) && a != null) {
+      return a;
+    }
     return SnackBarThemeData(
       backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t),
       actionTextColor: Color.lerp(a?.actionTextColor, b?.actionTextColor, t),
diff --git a/framework/lib/src/material/switch_theme.dart b/framework/lib/src/material/switch_theme.dart
index 4d204fe..8566d28 100644
--- a/framework/lib/src/material/switch_theme.dart
+++ b/framework/lib/src/material/switch_theme.dart
@@ -116,6 +116,9 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static SwitchThemeData lerp(SwitchThemeData? a, SwitchThemeData? b, double t) {
+    if (identical(a, b) && a != null) {
+      return a;
+    }
     return SwitchThemeData(
       thumbColor: MaterialStateProperty.lerp<Color?>(a?.thumbColor, b?.thumbColor, t, Color.lerp),
       trackColor: MaterialStateProperty.lerp<Color?>(a?.trackColor, b?.trackColor, t, Color.lerp),
diff --git a/framework/lib/src/material/tab_bar_theme.dart b/framework/lib/src/material/tab_bar_theme.dart
index 3c011d1..ebd0eb4 100644
--- a/framework/lib/src/material/tab_bar_theme.dart
+++ b/framework/lib/src/material/tab_bar_theme.dart
@@ -133,6 +133,9 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static TabBarTheme lerp(TabBarTheme a, TabBarTheme b, double t) {
+    if (identical(a, b)) {
+      return a;
+    }
     return TabBarTheme(
       indicator: Decoration.lerp(a.indicator, b.indicator, t),
       indicatorColor: Color.lerp(a.indicatorColor, b.indicatorColor, t),
diff --git a/framework/lib/src/material/tab_controller.dart b/framework/lib/src/material/tab_controller.dart
index 390b5cb..a323769 100644
--- a/framework/lib/src/material/tab_controller.dart
+++ b/framework/lib/src/material/tab_controller.dart
@@ -101,16 +101,20 @@
   ///
   /// The `initialIndex` must be valid given [length] and must not be null. If
   /// [length] is zero, then `initialIndex` must be 0 (the default).
-  TabController({ int initialIndex = 0, Duration? animationDuration, required this.length, required TickerProvider vsync})
-    : assert(length >= 0),
-      assert(initialIndex >= 0 && (length == 0 || initialIndex < length)),
-      _index = initialIndex,
-      _previousIndex = initialIndex,
-      _animationDuration = animationDuration ?? kTabScrollDuration,
-      _animationController = AnimationController.unbounded(
-        value: initialIndex.toDouble(),
-        vsync: vsync,
-      );
+  TabController({
+    int initialIndex = 0,
+    Duration? animationDuration,
+    required this.length,
+    required TickerProvider vsync,
+  }) : assert(length >= 0),
+       assert(initialIndex >= 0 && (length == 0 || initialIndex < length)),
+       _index = initialIndex,
+       _previousIndex = initialIndex,
+       _animationDuration = animationDuration ?? kTabScrollDuration,
+       _animationController = AnimationController.unbounded(
+         value: initialIndex.toDouble(),
+         vsync: vsync,
+       );
 
   // Private constructor used by `_copyWith`. This allows a new TabController to
   // be created without having to create a new animationController.
diff --git a/framework/lib/src/material/tabs.dart b/framework/lib/src/material/tabs.dart
index ebac40f..9175aaa 100644
--- a/framework/lib/src/material/tabs.dart
+++ b/framework/lib/src/material/tabs.dart
@@ -442,8 +442,8 @@
 
     if (!(rect.size >= insets.collapsedSize)) {
       throw FlutterError(
-          'indicatorPadding insets should be less than Tab Size\n'
-          'Rect Size : ${rect.size}, Insets: $insets',
+        'indicatorPadding insets should be less than Tab Size\n'
+        'Rect Size : ${rect.size}, Insets: $insets',
       );
     }
     return insets.deflateRect(rect);
@@ -562,7 +562,7 @@
 
   bool _viewportDimensionWasNonZero = false;
 
-  // Position should be adjusted at least once.
+  // The scroll position should be adjusted at least once.
   bool _needsPixelsCorrection = true;
 
   @override
@@ -573,11 +573,10 @@
     }
     // If the viewport never had a non-zero dimension, we just want to jump
     // to the initial scroll position to avoid strange scrolling effects in
-    // release mode: In release mode, the viewport temporarily may have a
-    // dimension of zero before the actual dimension is calculated. In that
-    // scenario, setting the actual dimension would cause a strange scroll
-    // effect without this guard because the super call below would starts a
-    // ballistic scroll activity.
+    // release mode: the viewport temporarily may have a dimension of zero
+    // before the actual dimension is calculated. In that scenario, setting
+    // the actual dimension would cause a strange scroll effect without this
+    // guard because the super call below would start a ballistic scroll activity.
     if (!_viewportDimensionWasNonZero || _needsPixelsCorrection) {
       _needsPixelsCorrection = false;
       correctPixels(tabBar._initialScrollOffset(viewportDimension, minScrollExtent, maxScrollExtent));
@@ -643,7 +642,7 @@
 /// See also:
 ///
 ///  * [TabBarView], which displays page views that correspond to each tab.
-///  * [TabBar], which is used to display the [Tab] that corresponds to each page of the [TabBarView].
+///  * [TabController], which coordinates tab selection between a [TabBar] and a [TabBarView].
 class TabBar extends StatefulWidget implements PreferredSizeWidget {
   /// Creates a Material Design tab bar.
   ///
@@ -658,7 +657,7 @@
   /// The [indicatorPadding] parameter defaults to [EdgeInsets.zero], and must not be null.
   ///
   /// If [indicator] is not null or provided from [TabBarTheme],
-  /// then [indicatorWeight], [indicatorPadding], and [indicatorColor] are ignored.
+  /// then [indicatorWeight] and [indicatorColor] are ignored.
   const TabBar({
     super.key,
     required this.tabs,
@@ -708,8 +707,8 @@
 
   /// The amount of space by which to inset the tab bar.
   ///
-  /// When [isScrollable] is false, this will yield the same result as if you had wrapped your
-  /// [TabBar] in a [Padding] widget. When [isScrollable] is true, the scrollable itself is inset,
+  /// When [isScrollable] is false, this will yield the same result as if [TabBar] was wrapped
+  /// in a [Padding] widget. When [isScrollable] is true, the scrollable itself is inset,
   /// allowing the padding to scroll with the tab bar, rather than enclosing it.
   final EdgeInsetsGeometry? padding;
 
@@ -731,22 +730,19 @@
   /// this property is ignored.
   final double indicatorWeight;
 
-
-  /// Padding for indicator.
-  /// This property will now no longer be ignored even if indicator is declared
-  /// or provided by [TabBarTheme]
+  /// The padding for the indicator.
+  ///
+  /// The default value of this property is [EdgeInsets.zero].
   ///
   /// For [isScrollable] tab bars, specifying [kTabLabelPadding] will align
   /// the indicator with the tab's text for [Tab] widgets and all but the
   /// shortest [Tab.text] values.
-  ///
-  /// The default value of [indicatorPadding] is [EdgeInsets.zero].
   final EdgeInsetsGeometry indicatorPadding;
 
   /// Defines the appearance of the selected tab indicator.
   ///
   /// If [indicator] is specified or provided from [TabBarTheme],
-  /// the [indicatorColor], and [indicatorWeight] properties are ignored.
+  /// the [indicatorColor] and [indicatorWeight] properties are ignored.
   ///
   /// The default, underline-style, selected tab indicator can be defined with
   /// [UnderlineTabIndicator].
@@ -765,6 +761,8 @@
 
   /// Whether this tab bar should automatically adjust the [indicatorColor].
   ///
+  /// The default value of this property is true.
+  ///
   /// If [automaticIndicatorColorAdjustment] is true,
   /// then the [indicatorColor] will be automatically adjusted to [Colors.white]
   /// when the [indicatorColor] is same as [Material.color] of the [Material]
@@ -802,8 +800,8 @@
   /// [MaterialState.selected] state, i.e. if the [Tab] is selected or not,
   /// ignoring [unselectedLabelColor] even if it's non-null.
   ///
-  /// Note: [labelStyle]'s color and [TabBarTheme.labelStyle]'s color do not
-  /// affect the effective [labelColor].
+  /// The color specified in the [labelStyle] and the [TabBarTheme.labelStyle]
+  /// do not affect the effective [labelColor].
   ///
   /// See also:
   ///
@@ -822,9 +820,9 @@
   /// will be used, otherwise unselected tab labels are rendered with
   /// [labelColor] at 70% opacity.
   ///
-  /// Note: [unselectedLabelStyle]'s color and
-  /// [TabBarTheme.unselectedLabelStyle]'s color are ignored in
-  /// [unselectedLabelColor]'s precedence calculation.
+  /// The color specified in the [unselectedLabelStyle] and the
+  /// [TabBarTheme.unselectedLabelStyle] are ignored in [unselectedLabelColor]'s
+  /// precedence calculation.
   ///
   /// See also:
   ///
@@ -1122,7 +1120,7 @@
       indicatorPadding: widget.indicatorPadding,
       tabKeys: _tabKeys,
       old: _indicatorPainter,
-      dividerColor: theme.useMaterial3 ? widget.dividerColor ?? defaults.dividerColor : null,
+      dividerColor: theme.useMaterial3 ? widget.dividerColor ?? tabBarTheme.dividerColor ?? defaults.dividerColor : null,
       labelPaddings: _labelPaddings,
     );
   }
diff --git a/framework/lib/src/material/text_button.dart b/framework/lib/src/material/text_button.dart
index cd9966a..0687a5a 100644
--- a/framework/lib/src/material/text_button.dart
+++ b/framework/lib/src/material/text_button.dart
@@ -270,7 +270,7 @@
   /// * `shadowColor` - Theme.shadowColor
   /// * `elevation` - 0
   /// * `padding`
-  ///   * `textScaleFactor <= 1` - all(8)
+  ///   * `textScaleFactor <= 1` - (horizontal(12), vertical(8))
   ///   * `1 < textScaleFactor <= 2` - lerp(all(8), horizontal(8))
   ///   * `2 < textScaleFactor <= 3` - lerp(horizontal(8), horizontal(4))
   ///   * `3 < textScaleFactor` - horizontal(4)
@@ -320,7 +320,7 @@
   /// * `surfaceTintColor` - null
   /// * `elevation` - 0
   /// * `padding`
-  ///   * `textScaleFactor <= 1` - all(8)
+  ///   * `textScaleFactor <= 1` - lerp(horizontal(12), horizontal(4))
   ///   * `1 < textScaleFactor <= 2` - lerp(all(8), horizontal(8))
   ///   * `2 < textScaleFactor <= 3` - lerp(horizontal(8), horizontal(4))
   ///   * `3 < textScaleFactor` - horizontal(4)
@@ -338,6 +338,9 @@
   /// * `enableFeedback` - true
   /// * `alignment` - Alignment.center
   /// * `splashFactory` - Theme.splashFactory
+  ///
+  /// For the [TextButton.icon] factory, the end (generally the right) value of
+  /// [padding] is increased from 12 to 16.
   /// {@endtemplate}
   @override
   ButtonStyle defaultStyleOf(BuildContext context) {
@@ -378,8 +381,9 @@
 }
 
 EdgeInsetsGeometry _scaledPadding(BuildContext context) {
+  final bool useMaterial3 = Theme.of(context).useMaterial3;
   return ButtonStyleButton.scaledPadding(
-    const EdgeInsets.all(8),
+    useMaterial3 ? const EdgeInsets.symmetric(horizontal: 12, vertical: 8) :  const EdgeInsets.all(8),
     const EdgeInsets.symmetric(horizontal: 8),
     const EdgeInsets.symmetric(horizontal: 4),
     MediaQuery.textScaleFactorOf(context),
@@ -489,8 +493,9 @@
 
   @override
   ButtonStyle defaultStyleOf(BuildContext context) {
+    final bool useMaterial3 = Theme.of(context).useMaterial3;
     final EdgeInsetsGeometry scaledPadding = ButtonStyleButton.scaledPadding(
-      const EdgeInsets.all(8),
+      useMaterial3 ? const EdgeInsetsDirectional.fromSTEB(12, 8, 16, 8) : const EdgeInsets.all(8),
       const EdgeInsets.symmetric(horizontal: 4),
       const EdgeInsets.symmetric(horizontal: 4),
       MediaQuery.textScaleFactorOf(context),
diff --git a/framework/lib/src/material/text_button_theme.dart b/framework/lib/src/material/text_button_theme.dart
index 130817e..f834e4c 100644
--- a/framework/lib/src/material/text_button_theme.dart
+++ b/framework/lib/src/material/text_button_theme.dart
@@ -49,8 +49,8 @@
 
   /// Linearly interpolate between two text button themes.
   static TextButtonThemeData? lerp(TextButtonThemeData? a, TextButtonThemeData? b, double t) {
-    if (a == null && b == null) {
-      return null;
+    if (identical(a, b)) {
+      return a;
     }
     return TextButtonThemeData(
       style: ButtonStyle.lerp(a?.style, b?.style, t),
diff --git a/framework/lib/src/material/text_field.dart b/framework/lib/src/material/text_field.dart
index eeffe03..505d6d8 100644
--- a/framework/lib/src/material/text_field.dart
+++ b/framework/lib/src/material/text_field.dart
@@ -1125,8 +1125,7 @@
       case TargetPlatform.windows:
       case TargetPlatform.fuchsia:
       case TargetPlatform.android:
-        if (cause == SelectionChangedCause.longPress
-            || cause == SelectionChangedCause.drag) {
+        if (cause == SelectionChangedCause.longPress) {
           _editableText?.bringIntoView(selection.extent);
         }
         break;
diff --git a/framework/lib/src/material/text_selection_theme.dart b/framework/lib/src/material/text_selection_theme.dart
index 592019b..9f586d4 100644
--- a/framework/lib/src/material/text_selection_theme.dart
+++ b/framework/lib/src/material/text_selection_theme.dart
@@ -73,8 +73,8 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static TextSelectionThemeData? lerp(TextSelectionThemeData? a, TextSelectionThemeData? b, double t) {
-    if (a == null && b == null) {
-      return null;
+    if (identical(a, b)) {
+      return a;
     }
     return TextSelectionThemeData(
       cursorColor: Color.lerp(a?.cursorColor, b?.cursorColor, t),
diff --git a/framework/lib/src/material/text_theme.dart b/framework/lib/src/material/text_theme.dart
index 42aefb8..6ede902 100644
--- a/framework/lib/src/material/text_theme.dart
+++ b/framework/lib/src/material/text_theme.dart
@@ -82,9 +82,9 @@
   /// If you do decide to create your own text theme, consider using one of
   /// those predefined themes as a starting point for [copyWith] or [apply].
   ///
-  /// Please note that you can not mix and match the 2018 styles with the 2021
-  /// styles. Only one or the other is allowed in this constructor. The 2018
-  /// styles will be deprecated and removed eventually.
+  /// The 2018 styles cannot be mixed with the 2021 styles. Only one or the
+  /// other is allowed in this constructor. The 2018 styles are deprecated and
+  /// will eventually be removed.
   const TextTheme({
     TextStyle? displayLarge,
     TextStyle? displayMedium,
@@ -799,6 +799,9 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static TextTheme lerp(TextTheme? a, TextTheme? b, double t) {
+    if (identical(a, b) && a != null) {
+      return a;
+    }
     return TextTheme(
       displayLarge: TextStyle.lerp(a?.displayLarge, b?.displayLarge, t),
       displayMedium: TextStyle.lerp(a?.displayMedium, b?.displayMedium, t),
diff --git a/framework/lib/src/material/theme_data.dart b/framework/lib/src/material/theme_data.dart
index 1f85ff4..d469d08 100644
--- a/framework/lib/src/material/theme_data.dart
+++ b/framework/lib/src/material/theme_data.dart
@@ -7,6 +7,8 @@
 import 'package:flute/cupertino.dart';
 import 'package:flute/foundation.dart';
 
+import 'action_buttons.dart';
+import 'action_icons_theme.dart';
 import 'app_bar_theme.dart';
 import 'badge_theme.dart';
 import 'banner_theme.dart';
@@ -337,6 +339,7 @@
     TextTheme? textTheme,
     Typography? typography,
     // COMPONENT THEMES
+    ActionIconThemeData? actionIconTheme,
     AppBarTheme? appBarTheme,
     BadgeThemeData? badgeTheme,
     MaterialBannerThemeData? bannerTheme,
@@ -382,13 +385,6 @@
     TooltipThemeData? tooltipTheme,
     // DEPRECATED (newest deprecations at the bottom)
     @Deprecated(
-      'Use colorScheme.secondary instead. '
-      'For more information, consult the migration guide at '
-      'https://flutter.dev/docs/release/breaking-changes/theme-data-accent-properties#migration-guide. '
-      'This feature was deprecated after v2.3.0-0.1.pre.',
-    )
-    Color? accentColor,
-    @Deprecated(
       'This "fix" is now enabled by default. '
       'This feature was deprecated after v2.5.0-1.0.pre.',
     )
@@ -478,7 +474,6 @@
       primaryColor ??= primarySurfaceColor;
       primaryColorBrightness = ThemeData.estimateBrightnessForColor(primarySurfaceColor);
       canvasColor ??= colorScheme.background;
-      accentColor ??= colorScheme.secondary;
       scaffoldBackgroundColor ??= colorScheme.background;
       bottomAppBarColor ??= colorScheme.surface;
       cardColor ??= colorScheme.surface;
@@ -496,8 +491,7 @@
     primaryColorLight ??= isDark ? Colors.grey[500]! : primarySwatch[100]!;
     primaryColorDark ??= isDark ? Colors.black : primarySwatch[700]!;
     final bool primaryIsDark = estimatedPrimaryColorBrightness == Brightness.dark;
-    toggleableActiveColor ??= isDark ? Colors.tealAccent[200]! : (accentColor ?? primarySwatch[600]!);
-    accentColor ??= isDark ? Colors.tealAccent[200]! : primarySwatch[500]!;
+    toggleableActiveColor ??= isDark ? Colors.tealAccent[200]! : (colorScheme?.secondary ?? primarySwatch[600]!);
     focusColor ??= isDark ? Colors.white.withOpacity(0.12) : Colors.black.withOpacity(0.12);
     hoverColor ??= isDark ? Colors.white.withOpacity(0.04) : Colors.black.withOpacity(0.04);
     shadowColor ??= Colors.black;
@@ -510,7 +504,7 @@
     colorScheme ??= ColorScheme.fromSwatch(
       primarySwatch: primarySwatch,
       primaryColorDark: primaryColorDark,
-      accentColor: accentColor,
+      accentColor: isDark ? Colors.tealAccent[200]! : primarySwatch[500]!,
       cardColor: cardColor,
       backgroundColor: isDark ? Colors.grey[700]! : primarySwatch[200]!,
       errorColor: Colors.red[700],
@@ -521,7 +515,7 @@
     // Spec doesn't specify a dark theme secondaryHeaderColor, this is a guess.
     secondaryHeaderColor ??= isDark ? Colors.grey[700]! : primarySwatch[50]!;
     dialogBackgroundColor ??= isDark ? Colors.grey[800]! : Colors.white;
-    indicatorColor ??= accentColor == primaryColor ? Colors.white : accentColor;
+    indicatorColor ??= colorScheme.secondary == primaryColor ? Colors.white : colorScheme.secondary;
     hintColor ??= isDark ? Colors.white60 : Colors.black.withOpacity(0.6);
     // The default [buttonTheme] is here because it doesn't use the defaults for
     // [disabledColor], [highlightColor], and [splashColor].
@@ -658,6 +652,7 @@
       typography: typography,
       primaryIconTheme: primaryIconTheme,
       // COMPONENT THEMES
+      actionIconTheme: actionIconTheme,
       appBarTheme: appBarTheme,
       badgeTheme: badgeTheme,
       bannerTheme: bannerTheme,
@@ -702,7 +697,6 @@
       toggleButtonsTheme: toggleButtonsTheme,
       tooltipTheme: tooltipTheme,
       // DEPRECATED (newest deprecations at the bottom)
-      accentColor: accentColor,
       fixTextFieldOutlineLabel: fixTextFieldOutlineLabel,
       primaryColorBrightness: primaryColorBrightness,
       androidOverscrollIndicator: androidOverscrollIndicator,
@@ -769,6 +763,7 @@
     required this.textTheme,
     required this.typography,
     // COMPONENT THEMES
+    required this.actionIconTheme,
     required this.appBarTheme,
     required this.badgeTheme,
     required this.bannerTheme,
@@ -814,13 +809,6 @@
     required this.tooltipTheme,
     // DEPRECATED (newest deprecations at the bottom)
     @Deprecated(
-      'Use colorScheme.secondary instead. '
-      'For more information, consult the migration guide at '
-      'https://flutter.dev/docs/release/breaking-changes/theme-data-accent-properties#migration-guide. '
-      'This feature was deprecated after v2.3.0-0.1.pre.',
-    )
-    Color? accentColor,
-    @Deprecated(
       'This "fix" is now enabled by default. '
       'This feature was deprecated after v2.5.0-1.0.pre.',
     )
@@ -865,7 +853,6 @@
 
   }) : // DEPRECATED (newest deprecations at the bottom)
        // should not be `required`, use getter pattern to avoid breakages.
-       _accentColor = accentColor,
        _fixTextFieldOutlineLabel = fixTextFieldOutlineLabel,
        _primaryColorBrightness = primaryColorBrightness,
        _toggleableActiveColor = toggleableActiveColor,
@@ -875,7 +862,6 @@
        _bottomAppBarColor = bottomAppBarColor,
        assert(toggleableActiveColor != null),
         // DEPRECATED (newest deprecations at the bottom)
-       assert(accentColor != null),
        assert(fixTextFieldOutlineLabel != null),
        assert(primaryColorBrightness != null),
        assert(errorColor != null),
@@ -926,7 +912,6 @@
       primaryColor: primarySurfaceColor,
       primaryColorBrightness: ThemeData.estimateBrightnessForColor(primarySurfaceColor),
       canvasColor: colorScheme.background,
-      accentColor: colorScheme.secondary,
       scaffoldBackgroundColor: colorScheme.background,
       bottomAppBarColor: colorScheme.surface,
       cardColor: colorScheme.surface,
@@ -1138,10 +1123,11 @@
   /// will aim to only support Material 3.
   ///
   /// ## Defaults
-  /// If a [ThemeData] is constructed with [useMaterial3] set to true, then
-  /// some properties will get updated defaults. Please note that
-  /// [ThemeData.copyWith] with [useMaterial3] set to true will
-  /// not change any of these properties in the resulting [ThemeData].
+  ///
+  /// If a [ThemeData] is _constructed_ with [useMaterial3] set to true, then
+  /// some properties will get updated defaults. However, the
+  /// [ThemeData.copyWith] method with [useMaterial3] set to true will _not_
+  /// change any of these properties in the resulting [ThemeData].
   ///
   /// <style>table,td,th { border-collapse: collapse; padding: 0.45em; } td { border: 1px solid }</style>
   ///
@@ -1375,6 +1361,10 @@
 
   // COMPONENT THEMES
 
+  /// A theme for customizing icons of [BackButtonIcon], [CloseButtonIcon],
+  /// [DrawerButtonIcon], or [EndDrawerButtonIcon].
+  final ActionIconThemeData? actionIconTheme;
+
   /// A theme for customizing the color, elevation, brightness, iconTheme and
   /// textTheme of [AppBar]s.
   final AppBarTheme appBarTheme;
@@ -1533,25 +1523,6 @@
 
   // DEPRECATED (newest deprecations at the bottom)
 
-  /// Obsolete property that was originally used as the foreground
-  /// color for widgets (knobs, text, overscroll edge effect, etc).
-  ///
-  /// The material library no longer uses this property. In most cases the
-  /// [colorScheme]'s [ColorScheme.secondary] property is now used instead.
-  ///
-  /// Apps should migrate uses of this property to the theme's [colorScheme]
-  /// [ColorScheme.secondary] color. In cases where a color is needed that
-  /// contrasts well with the secondary color [ColorScheme.onSecondary]
-  /// can be used.
-  @Deprecated(
-    'Use colorScheme.secondary instead. '
-    'For more information, consult the migration guide at '
-    'https://flutter.dev/docs/release/breaking-changes/theme-data-accent-properties#migration-guide. '
-    'This feature was deprecated after v2.3.0-0.1.pre.',
-  )
-  Color get accentColor => _accentColor!;
-  final Color? _accentColor;
-
   /// An obsolete flag to allow apps to opt-out of a
   /// [small fix](https://github.com/flutter/flutter/issues/54028) for the Y
   /// coordinate of the floating label in a [TextField] [OutlineInputBorder].
@@ -1692,6 +1663,7 @@
     TextTheme? textTheme,
     Typography? typography,
     // COMPONENT THEMES
+    ActionIconThemeData? actionIconTheme,
     AppBarTheme? appBarTheme,
     BadgeThemeData? badgeTheme,
     MaterialBannerThemeData? bannerTheme,
@@ -1737,13 +1709,6 @@
     TooltipThemeData? tooltipTheme,
     // DEPRECATED (newest deprecations at the bottom)
     @Deprecated(
-      'No longer used by the framework, please remove any reference to it. '
-      'For more information, consult the migration guide at '
-      'https://flutter.dev/docs/release/breaking-changes/theme-data-accent-properties#migration-guide. '
-      'This feature was deprecated after v2.3.0-0.1.pre.',
-    )
-    Color? accentColor,
-    @Deprecated(
       'This "fix" is now enabled by default. '
       'This feature was deprecated after v2.5.0-1.0.pre.',
     )
@@ -1832,6 +1797,7 @@
       textTheme: textTheme ?? this.textTheme,
       typography: typography ?? this.typography,
       // COMPONENT THEMES
+      actionIconTheme: actionIconTheme ?? this.actionIconTheme,
       appBarTheme: appBarTheme ?? this.appBarTheme,
       badgeTheme: badgeTheme ?? this.badgeTheme,
       bannerTheme: bannerTheme ?? this.bannerTheme,
@@ -1876,7 +1842,6 @@
       toggleButtonsTheme: toggleButtonsTheme ?? this.toggleButtonsTheme,
       tooltipTheme: tooltipTheme ?? this.tooltipTheme,
       // DEPRECATED (newest deprecations at the bottom)
-      accentColor: accentColor ?? _accentColor,
       fixTextFieldOutlineLabel: fixTextFieldOutlineLabel ?? _fixTextFieldOutlineLabel,
       primaryColorBrightness: primaryColorBrightness ?? _primaryColorBrightness,
       androidOverscrollIndicator: androidOverscrollIndicator ?? this.androidOverscrollIndicator,
@@ -1978,6 +1943,9 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static ThemeData lerp(ThemeData a, ThemeData b, double t) {
+    if (identical(a, b)) {
+      return a;
+    }
     return ThemeData.raw(
       // For the sanity of the reader, make sure these properties are in the same
       // order in every place that they are separated by section comments (e.g.
@@ -2023,6 +1991,7 @@
       textTheme: TextTheme.lerp(a.textTheme, b.textTheme, t),
       typography: Typography.lerp(a.typography, b.typography, t),
       // COMPONENT THEMES
+      actionIconTheme: ActionIconThemeData.lerp(a.actionIconTheme, b.actionIconTheme, t),
       appBarTheme: AppBarTheme.lerp(a.appBarTheme, b.appBarTheme, t),
       badgeTheme: BadgeThemeData.lerp(a.badgeTheme, b.badgeTheme, t),
       bannerTheme: MaterialBannerThemeData.lerp(a.bannerTheme, b.bannerTheme, t),
@@ -2067,7 +2036,6 @@
       toggleButtonsTheme: ToggleButtonsThemeData.lerp(a.toggleButtonsTheme, b.toggleButtonsTheme, t)!,
       tooltipTheme: TooltipThemeData.lerp(a.tooltipTheme, b.tooltipTheme, t)!,
       // DEPRECATED (newest deprecations at the bottom)
-      accentColor: Color.lerp(a.accentColor, b.accentColor, t),
       fixTextFieldOutlineLabel: t < 0.5 ? a.fixTextFieldOutlineLabel : b.fixTextFieldOutlineLabel,
       primaryColorBrightness: t < 0.5 ? a.primaryColorBrightness : b.primaryColorBrightness,
       androidOverscrollIndicator:t < 0.5 ? a.androidOverscrollIndicator : b.androidOverscrollIndicator,
@@ -2129,6 +2097,7 @@
         other.textTheme == textTheme &&
         other.typography == typography &&
         // COMPONENT THEMES
+        other.actionIconTheme == actionIconTheme &&
         other.appBarTheme == appBarTheme &&
         other.badgeTheme == badgeTheme &&
         other.bannerTheme == bannerTheme &&
@@ -2173,7 +2142,6 @@
         other.toggleButtonsTheme == toggleButtonsTheme &&
         other.tooltipTheme == tooltipTheme &&
         // DEPRECATED (newest deprecations at the bottom)
-        other.accentColor == accentColor &&
         other.fixTextFieldOutlineLabel == fixTextFieldOutlineLabel &&
         other.primaryColorBrightness == primaryColorBrightness &&
         other.androidOverscrollIndicator == androidOverscrollIndicator &&
@@ -2232,6 +2200,7 @@
       textTheme,
       typography,
       // COMPONENT THEMES
+      actionIconTheme,
       appBarTheme,
       badgeTheme,
       bannerTheme,
@@ -2276,7 +2245,6 @@
       toggleButtonsTheme,
       tooltipTheme,
       // DEPRECATED (newest deprecations at the bottom)
-      accentColor,
       fixTextFieldOutlineLabel,
       primaryColorBrightness,
       androidOverscrollIndicator,
@@ -2337,6 +2305,7 @@
     properties.add(DiagnosticsProperty<TextTheme>('textTheme', textTheme, level: DiagnosticLevel.debug));
     properties.add(DiagnosticsProperty<Typography>('typography', typography, defaultValue: defaultData.typography, level: DiagnosticLevel.debug));
     // COMPONENT THEMES
+    properties.add(DiagnosticsProperty<ActionIconThemeData>('actionIconTheme', actionIconTheme, level: DiagnosticLevel.debug));
     properties.add(DiagnosticsProperty<AppBarTheme>('appBarTheme', appBarTheme, defaultValue: defaultData.appBarTheme, level: DiagnosticLevel.debug));
     properties.add(DiagnosticsProperty<BadgeThemeData>('badgeTheme', badgeTheme, defaultValue: defaultData.badgeTheme, level: DiagnosticLevel.debug));
     properties.add(DiagnosticsProperty<MaterialBannerThemeData>('bannerTheme', bannerTheme, defaultValue: defaultData.bannerTheme, level: DiagnosticLevel.debug));
@@ -2381,7 +2350,6 @@
     properties.add(DiagnosticsProperty<ToggleButtonsThemeData>('toggleButtonsTheme', toggleButtonsTheme, level: DiagnosticLevel.debug));
     properties.add(DiagnosticsProperty<TooltipThemeData>('tooltipTheme', tooltipTheme, level: DiagnosticLevel.debug));
     // DEPRECATED (newest deprecations at the bottom)
-    properties.add(ColorProperty('accentColor', accentColor, defaultValue: defaultData.accentColor, level: DiagnosticLevel.debug));
     properties.add(DiagnosticsProperty<bool>('fixTextFieldOutlineLabel', fixTextFieldOutlineLabel, level: DiagnosticLevel.debug));
     properties.add(EnumProperty<Brightness>('primaryColorBrightness', primaryColorBrightness, defaultValue: defaultData.primaryColorBrightness, level: DiagnosticLevel.debug));
     properties.add(EnumProperty<AndroidOverscrollIndicator>('androidOverscrollIndicator', androidOverscrollIndicator, defaultValue: null, level: DiagnosticLevel.debug));
@@ -2732,6 +2700,9 @@
 
   /// Linearly interpolate between two densities.
   static VisualDensity lerp(VisualDensity a, VisualDensity b, double t) {
+    if (identical(a, b)) {
+      return a;
+    }
     return VisualDensity(
       horizontal: lerpDouble(a.horizontal, b.horizontal, t)!,
       vertical: lerpDouble(a.vertical, b.vertical, t)!,
diff --git a/framework/lib/src/material/time_picker_theme.dart b/framework/lib/src/material/time_picker_theme.dart
index e2312b8..d35caa2 100644
--- a/framework/lib/src/material/time_picker_theme.dart
+++ b/framework/lib/src/material/time_picker_theme.dart
@@ -299,7 +299,9 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static TimePickerThemeData lerp(TimePickerThemeData? a, TimePickerThemeData? b, double t) {
-
+    if (identical(a, b) && a != null) {
+      return a;
+    }
     // Workaround since BorderSide's lerp does not allow for null arguments.
     BorderSide? lerpedBorderSide;
     if (a?.dayPeriodBorderSide == null && b?.dayPeriodBorderSide == null) {
diff --git a/framework/lib/src/material/toggle_buttons_theme.dart b/framework/lib/src/material/toggle_buttons_theme.dart
index d4c9bd5..6cfa191 100644
--- a/framework/lib/src/material/toggle_buttons_theme.dart
+++ b/framework/lib/src/material/toggle_buttons_theme.dart
@@ -150,8 +150,8 @@
 
   /// Linearly interpolate between two toggle buttons themes.
   static ToggleButtonsThemeData? lerp(ToggleButtonsThemeData? a, ToggleButtonsThemeData? b, double t) {
-    if (a == null && b == null) {
-      return null;
+    if (identical(a, b)) {
+      return a;
     }
     return ToggleButtonsThemeData(
       textStyle: TextStyle.lerp(a?.textStyle, b?.textStyle, t),
diff --git a/framework/lib/src/material/tooltip_theme.dart b/framework/lib/src/material/tooltip_theme.dart
index 53cde10..f917962 100644
--- a/framework/lib/src/material/tooltip_theme.dart
+++ b/framework/lib/src/material/tooltip_theme.dart
@@ -149,8 +149,8 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static TooltipThemeData? lerp(TooltipThemeData? a, TooltipThemeData? b, double t) {
-    if (a == null && b == null) {
-      return null;
+    if (identical(a, b)) {
+      return a;
     }
     return TooltipThemeData(
       height: lerpDouble(a?.height, b?.height, t),
diff --git a/framework/lib/src/material/typography.dart b/framework/lib/src/material/typography.dart
index 0a08ffc..093311b 100644
--- a/framework/lib/src/material/typography.dart
+++ b/framework/lib/src/material/typography.dart
@@ -329,6 +329,9 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static Typography lerp(Typography a, Typography b, double t) {
+    if (identical(a, b)) {
+      return a;
+    }
     return Typography._(
       TextTheme.lerp(a.black, b.black, t),
       TextTheme.lerp(a.white, b.white, t),
diff --git a/framework/lib/src/painting/alignment.dart b/framework/lib/src/painting/alignment.dart
index 4149485..eff4d75 100644
--- a/framework/lib/src/painting/alignment.dart
+++ b/framework/lib/src/painting/alignment.dart
@@ -87,8 +87,8 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static AlignmentGeometry? lerp(AlignmentGeometry? a, AlignmentGeometry? b, double t) {
-    if (a == null && b == null) {
-      return null;
+    if (identical(a, b)) {
+      return a;
     }
     if (a == null) {
       return b! * t;
@@ -337,8 +337,8 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static Alignment? lerp(Alignment? a, Alignment? b, double t) {
-    if (a == null && b == null) {
-      return null;
+    if (identical(a, b)) {
+      return a;
     }
     if (a == null) {
       return Alignment(ui.lerpDouble(0.0, b!.x, t)!, ui.lerpDouble(0.0, b.y, t)!);
@@ -528,8 +528,8 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static AlignmentDirectional? lerp(AlignmentDirectional? a, AlignmentDirectional? b, double t) {
-    if (a == null && b == null) {
-      return null;
+    if (identical(a, b)) {
+      return a;
     }
     if (a == null) {
       return AlignmentDirectional(ui.lerpDouble(0.0, b!.start, t)!, ui.lerpDouble(0.0, b.y, t)!);
diff --git a/framework/lib/src/painting/basic_types.dart b/framework/lib/src/painting/basic_types.dart
index 6675055..b2c1fae 100644
--- a/framework/lib/src/painting/basic_types.dart
+++ b/framework/lib/src/painting/basic_types.dart
@@ -48,93 +48,18 @@
 
 export 'package:flute/foundation.dart' show VoidCallback;
 
-// Intentionally not exported:
-//  - Image, instantiateImageCodec, decodeImageFromList:
-//      We use ui.* to make it very explicit that these are low-level image APIs.
-//      Generally, higher layers provide more reasonable APIs around images.
-//  - lerpDouble:
-//      Hopefully this will eventually become Double.lerp.
-//  - Paragraph, ParagraphBuilder, ParagraphStyle, TextBox:
-//      These are low-level text primitives. Use this package's TextPainter API.
-//  - Picture, PictureRecorder, Scene, SceneBuilder:
-//      These are low-level primitives. Generally, the rendering layer makes these moot.
-//  - Gradient:
-//      Use this package's higher-level Gradient API instead.
-//  - window, WindowPadding
-//      These are generally wrapped by other APIs so we always refer to them directly
-//      as ui.* to avoid making them seem like high-level APIs.
-
-/// The description of the difference between two objects, in the context of how
-/// it will affect the rendering.
-///
-/// Used by [TextSpan.compareTo] and [TextStyle.compareTo].
-///
-/// The values in this enum are ordered such that they are in increasing order
-/// of cost. A value with index N implies all the values with index less than N.
-/// For example, [layout] (index 3) implies [paint] (2).
 enum RenderComparison {
-  /// The two objects are identical (meaning deeply equal, not necessarily
-  /// [dart:core.identical]).
   identical,
-
-  /// The two objects are identical for the purpose of layout, but may be different
-  /// in other ways.
-  ///
-  /// For example, maybe some event handlers changed.
   metadata,
-
-  /// The two objects are different but only in ways that affect paint, not layout.
-  ///
-  /// For example, only the color is changed.
-  ///
-  /// [RenderObject.markNeedsPaint] would be necessary to handle this kind of
-  /// change in a render object.
   paint,
-
-  /// The two objects are different in ways that affect layout (and therefore paint).
-  ///
-  /// For example, the size is changed.
-  ///
-  /// This is the most drastic level of change possible.
-  ///
-  /// [RenderObject.markNeedsLayout] would be necessary to handle this kind of
-  /// change in a render object.
   layout,
 }
 
-/// The two cardinal directions in two dimensions.
-///
-/// The axis is always relative to the current coordinate space. This means, for
-/// example, that a [horizontal] axis might actually be diagonally from top
-/// right to bottom left, due to some local [Transform] applied to the scene.
-///
-/// See also:
-///
-///  * [AxisDirection], which is a directional version of this enum (with values
-///    light left and right, rather than just horizontal).
-///  * [TextDirection], which disambiguates between left-to-right horizontal
-///    content and right-to-left horizontal content.
 enum Axis {
-  /// Left and right.
-  ///
-  /// See also:
-  ///
-  ///  * [TextDirection], which disambiguates between left-to-right horizontal
-  ///    content and right-to-left horizontal content.
   horizontal,
-
-  /// Up and down.
   vertical,
 }
 
-/// Returns the opposite of the given [Axis].
-///
-/// Specifically, returns [Axis.horizontal] for [Axis.vertical], and
-/// vice versa.
-///
-/// See also:
-///
-///  * [flipAxisDirection], which does the same thing for [AxisDirection] values.
 Axis flipAxis(Axis direction) {
   switch (direction) {
     case Axis.horizontal:
diff --git a/framework/lib/src/painting/border_radius.dart b/framework/lib/src/painting/border_radius.dart
index b1bdfaf..416423a 100644
--- a/framework/lib/src/painting/border_radius.dart
+++ b/framework/lib/src/painting/border_radius.dart
@@ -129,8 +129,8 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static BorderRadiusGeometry? lerp(BorderRadiusGeometry? a, BorderRadiusGeometry? b, double t) {
-    if (a == null && b == null) {
-      return null;
+    if (identical(a, b)) {
+      return a;
     }
     a ??= BorderRadius.zero;
     b ??= BorderRadius.zero;
@@ -506,8 +506,8 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static BorderRadius? lerp(BorderRadius? a, BorderRadius? b, double t) {
-    if (a == null && b == null) {
-      return null;
+    if (identical(a, b)) {
+      return a;
     }
     if (a == null) {
       return b! * t;
@@ -727,8 +727,8 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static BorderRadiusDirectional? lerp(BorderRadiusDirectional? a, BorderRadiusDirectional? b, double t) {
-    if (a == null && b == null) {
-      return null;
+    if (identical(a, b)) {
+      return a;
     }
     if (a == null) {
       return b! * t;
diff --git a/framework/lib/src/painting/borders.dart b/framework/lib/src/painting/borders.dart
index 4316028..08f934b 100644
--- a/framework/lib/src/painting/borders.dart
+++ b/framework/lib/src/painting/borders.dart
@@ -26,8 +26,8 @@
 /// A [Border] consists of four [BorderSide] objects: [Border.top],
 /// [Border.left], [Border.right], and [Border.bottom].
 ///
-/// Note that setting [BorderSide.width] to 0.0 will result in hairline
-/// rendering. A more involved explanation is present in [BorderSide.width].
+/// Setting [BorderSide.width] to 0.0 will result in hairline rendering; see
+/// [BorderSide.width] for a more involved explanation.
 ///
 /// {@tool snippet}
 /// This sample shows how [BorderSide] objects can be used in a [Container], via
@@ -108,7 +108,7 @@
   /// The width of this side of the border, in logical pixels.
   ///
   /// Setting width to 0.0 will result in a hairline border. This means that
-  /// the border will have the width of one physical pixel. Also, hairline
+  /// the border will have the width of one physical pixel. Hairline
   /// rendering takes shortcuts when the path overlaps a pixel more than once.
   /// This means that it will render faster than otherwise, but it might
   /// double-hit pixels, giving it a slightly darker/lighter result.
@@ -143,6 +143,11 @@
   /// - [strokeAlignCenter] provides padding with half [width].
   /// - [strokeAlignOutside] provides zero padding, as stroke is drawn entirely outside.
   ///
+  /// This property is not honored by [toPaint] (because the [Paint] object
+  /// cannot represent it); it is intended that classes that use [BorderSide]
+  /// objects implement this property when painting borders by suitably
+  /// inflating or deflating their regions.
+  ///
   /// {@tool dartpad}
   /// This example shows an animation of how [strokeAlign] affects the drawing
   /// when applied to borders of various shapes.
@@ -153,15 +158,21 @@
 
   /// The border is drawn fully inside of the border path.
   ///
-  /// This is the default.
+  /// This is a constant for use with [strokeAlign].
+  ///
+  /// This is the default value for [strokeAlign].
   static const double strokeAlignInside = -1.0;
 
   /// The border is drawn on the center of the border path, with half of the
   /// [BorderSide.width] on the inside, and the other half on the outside of
   /// the path.
+  ///
+  /// This is a constant for use with [strokeAlign].
   static const double strokeAlignCenter = 0.0;
 
   /// The border is drawn on the outside of the border path.
+  ///
+  /// This is a constant for use with [strokeAlign].
   static const double strokeAlignOutside = 1.0;
 
   /// Creates a copy of this border but with the given fields replaced with the new values.
@@ -206,6 +217,9 @@
   /// Create a [Paint] object that, if used to stroke a line, will draw the line
   /// in this border's style.
   ///
+  /// The [strokeAlign] property is not reflected in the [Paint]; consumers must
+  /// implement that directly by inflating or deflating their region appropriately.
+  ///
   /// Not all borders use this method to paint their border sides. For example,
   /// non-uniform rectangular [Border]s have beveled edges and so paint their
   /// border sides as filled shapes rather than using a stroke.
@@ -246,6 +260,9 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static BorderSide lerp(BorderSide a, BorderSide b, double t) {
+    if (identical(a, b)) {
+      return a;
+    }
     if (t == 0.0) {
       return a;
     }
@@ -505,6 +522,9 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static ShapeBorder? lerp(ShapeBorder? a, ShapeBorder? b, double t) {
+    if (identical(a, b)) {
+      return a;
+    }
     ShapeBorder? result;
     if (b != null) {
       result = b.lerpFrom(a, t);
@@ -694,6 +714,9 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static OutlinedBorder? lerp(OutlinedBorder? a, OutlinedBorder? b, double t) {
+    if (identical(a, b)) {
+      return a;
+    }
     ShapeBorder? result;
     if (b != null) {
       result = b.lerpFrom(a, t);
diff --git a/framework/lib/src/painting/box_border.dart b/framework/lib/src/painting/box_border.dart
index e5a4ffc..72ec572 100644
--- a/framework/lib/src/painting/box_border.dart
+++ b/framework/lib/src/painting/box_border.dart
@@ -103,6 +103,9 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static BoxBorder? lerp(BoxBorder? a, BoxBorder? b, double t) {
+    if (identical(a, b)) {
+      return a;
+    }
     if ((a is Border?) && (b is Border?)) {
       return Border.lerp(a, b, t);
     }
@@ -469,8 +472,8 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static Border? lerp(Border? a, Border? b, double t) {
-    if (a == null && b == null) {
-      return null;
+    if (identical(a, b)) {
+      return a;
     }
     if (a == null) {
       return b!.scale(t);
@@ -811,8 +814,8 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static BorderDirectional? lerp(BorderDirectional? a, BorderDirectional? b, double t) {
-    if (a == null && b == null) {
-      return null;
+    if (identical(a, b)) {
+      return a;
     }
     if (a == null) {
       return b!.scale(t);
diff --git a/framework/lib/src/painting/box_decoration.dart b/framework/lib/src/painting/box_decoration.dart
index a81eab4..f469060 100644
--- a/framework/lib/src/painting/box_decoration.dart
+++ b/framework/lib/src/painting/box_decoration.dart
@@ -288,8 +288,8 @@
   ///    and which use [BoxDecoration.lerp] when interpolating two
   ///    [BoxDecoration]s or a [BoxDecoration] to or from null.
   static BoxDecoration? lerp(BoxDecoration? a, BoxDecoration? b, double t) {
-    if (a == null && b == null) {
-      return null;
+    if (identical(a, b)) {
+      return a;
     }
     if (a == null) {
       return b!.scale(t);
diff --git a/framework/lib/src/painting/box_shadow.dart b/framework/lib/src/painting/box_shadow.dart
index f050709..4b474e8 100644
--- a/framework/lib/src/painting/box_shadow.dart
+++ b/framework/lib/src/painting/box_shadow.dart
@@ -86,8 +86,8 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static BoxShadow? lerp(BoxShadow? a, BoxShadow? b, double t) {
-    if (a == null && b == null) {
-      return null;
+    if (identical(a, b)) {
+      return a;
     }
     if (a == null) {
       return b!.scale(t);
@@ -110,8 +110,8 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static List<BoxShadow>? lerpList(List<BoxShadow>? a, List<BoxShadow>? b, double t) {
-    if (a == null && b == null) {
-      return null;
+    if (identical(a, b)) {
+      return a;
     }
     a ??= <BoxShadow>[];
     b ??= <BoxShadow>[];
diff --git a/framework/lib/src/painting/colors.dart b/framework/lib/src/painting/colors.dart
index 45a87fe..3ccbf50 100644
--- a/framework/lib/src/painting/colors.dart
+++ b/framework/lib/src/painting/colors.dart
@@ -194,8 +194,8 @@
   ///
   /// Values outside of the valid range for each channel will be clamped.
   static HSVColor? lerp(HSVColor? a, HSVColor? b, double t) {
-    if (a == null && b == null) {
-      return null;
+    if (identical(a, b)) {
+      return a;
     }
     if (a == null) {
       return b!._scaleAlpha(t);
@@ -377,8 +377,8 @@
   /// Values for `t` are usually obtained from an [Animation<double>], such as
   /// an [AnimationController].
   static HSLColor? lerp(HSLColor? a, HSLColor? b, double t) {
-    if (a == null && b == null) {
-      return null;
+    if (identical(a, b)) {
+      return a;
     }
     if (a == null) {
       return b!._scaleAlpha(t);
@@ -479,13 +479,12 @@
   /// Values for `t` are usually obtained from an [Animation<double>], such as
   /// an [AnimationController].
   static ColorSwatch<T>? lerp<T>(ColorSwatch<T>? a, ColorSwatch<T>? b, double t) {
+    if (identical(a, b)) {
+      return a;
+    }
     final Map<T, Color> swatch;
     if (b == null) {
-      if (a == null) {
-        return null;
-      } else {
-        swatch = a._swatch.map((T key, Color color) => MapEntry<T, Color>(key, Color.lerp(color, null, t)!));
-      }
+      swatch = a!._swatch.map((T key, Color color) => MapEntry<T, Color>(key, Color.lerp(color, null, t)!));
     } else {
       if (a == null) {
         swatch = b._swatch.map((T key, Color color) => MapEntry<T, Color>(key, Color.lerp(null, color, t)!));
diff --git a/framework/lib/src/painting/decoration.dart b/framework/lib/src/painting/decoration.dart
index 85ef326..677bea0 100644
--- a/framework/lib/src/painting/decoration.dart
+++ b/framework/lib/src/painting/decoration.dart
@@ -69,7 +69,7 @@
   ///
   /// When implementing this method in subclasses, return null if this class
   /// cannot interpolate from `a`. In that case, [lerp] will try `a`'s [lerpTo]
-  /// method instead.
+  /// method instead. Classes should implement both [lerpFrom] and [lerpTo].
   ///
   /// Supporting interpolating from null is recommended as the [Decoration.lerp]
   /// method uses this as a fallback when two classes can't interpolate between
@@ -95,11 +95,11 @@
   /// Linearly interpolates from `this` to another [Decoration] (which may be of
   /// a different class).
   ///
-  /// This is called if `b`'s [lerpTo] did not know how to handle this class.
+  /// This is called if `b`'s [lerpFrom] did not know how to handle this class.
   ///
   /// When implementing this method in subclasses, return null if this class
   /// cannot interpolate from `b`. In that case, [lerp] will apply a default
-  /// behavior instead.
+  /// behavior instead. Classes should implement both [lerpFrom] and [lerpTo].
   ///
   /// Supporting interpolating to null is recommended as the [Decoration.lerp]
   /// method uses this as a fallback when two classes can't interpolate between
@@ -129,8 +129,8 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static Decoration? lerp(Decoration? a, Decoration? b, double t) {
-    if (a == null && b == null) {
-      return null;
+    if (identical(a, b)) {
+      return a;
     }
     if (a == null) {
       return b!.lerpFrom(null, t) ?? b;
diff --git a/framework/lib/src/painting/edge_insets.dart b/framework/lib/src/painting/edge_insets.dart
index 26e9c2d..2a2d5dc 100644
--- a/framework/lib/src/painting/edge_insets.dart
+++ b/framework/lib/src/painting/edge_insets.dart
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import 'package:engine/ui.dart' as ui show WindowPadding, lerpDouble;
+import 'package:engine/ui.dart' as ui show ViewPadding, lerpDouble;
 
 import 'package:flute/foundation.dart';
 
@@ -215,8 +215,8 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static EdgeInsetsGeometry? lerp(EdgeInsetsGeometry? a, EdgeInsetsGeometry? b, double t) {
-    if (a == null && b == null) {
-      return null;
+    if (identical(a, b)) {
+      return a;
     }
     if (a == null) {
       return b! * t;
@@ -397,18 +397,27 @@
        right = horizontal,
        bottom = vertical;
 
-  /// Creates insets that match the given window padding.
+  /// Creates insets that match the given view padding.
   ///
   /// If you need the current system padding or view insets in the context of a
   /// widget, consider using [MediaQuery.of] to obtain these values rather than
-  /// using the value from [dart:ui.window], so that you get notified of
+  /// using the value from a [FlutterView] directly, so that you get notified of
   /// changes.
-  EdgeInsets.fromWindowPadding(ui.WindowPadding padding, double devicePixelRatio)
+  EdgeInsets.fromViewPadding(ui.ViewPadding padding, double devicePixelRatio)
     : left = padding.left / devicePixelRatio,
       top = padding.top / devicePixelRatio,
       right = padding.right / devicePixelRatio,
       bottom = padding.bottom / devicePixelRatio;
 
+  /// Deprecated. Will be removed in a future version of Flutter.
+  ///
+  /// Use [EdgeInsets.fromViewPadding] instead.
+  @Deprecated(
+    'Use EdgeInsets.fromViewPadding instead. '
+    'This feature was deprecated after v3.8.0-14.0.pre.',
+  )
+  factory EdgeInsets.fromWindowPadding(ui.ViewPadding padding, double devicePixelRatio) = EdgeInsets.fromViewPadding;
+
   /// An [EdgeInsets] with zero offsets in each direction.
   static const EdgeInsets zero = EdgeInsets.only();
 
@@ -602,8 +611,8 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static EdgeInsets? lerp(EdgeInsets? a, EdgeInsets? b, double t) {
-    if (a == null && b == null) {
-      return null;
+    if (identical(a, b)) {
+      return a;
     }
     if (a == null) {
       return b! * t;
@@ -868,8 +877,8 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static EdgeInsetsDirectional? lerp(EdgeInsetsDirectional? a, EdgeInsetsDirectional? b, double t) {
-    if (a == null && b == null) {
-      return null;
+    if (identical(a, b)) {
+      return a;
     }
     if (a == null) {
       return b! * t;
diff --git a/framework/lib/src/painting/flutter_logo.dart b/framework/lib/src/painting/flutter_logo.dart
index c3c31ee..ccef895 100644
--- a/framework/lib/src/painting/flutter_logo.dart
+++ b/framework/lib/src/painting/flutter_logo.dart
@@ -101,8 +101,8 @@
   static FlutterLogoDecoration? lerp(FlutterLogoDecoration? a, FlutterLogoDecoration? b, double t) {
     assert(a == null || a.debugAssertIsValid());
     assert(b == null || b.debugAssertIsValid());
-    if (a == null && b == null) {
-      return null;
+    if (identical(a, b)) {
+      return a;
     }
     if (a == null) {
       return FlutterLogoDecoration._(
diff --git a/framework/lib/src/painting/fractional_offset.dart b/framework/lib/src/painting/fractional_offset.dart
index d32957c..fce6603 100644
--- a/framework/lib/src/painting/fractional_offset.dart
+++ b/framework/lib/src/painting/fractional_offset.dart
@@ -176,8 +176,8 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static FractionalOffset? lerp(FractionalOffset? a, FractionalOffset? b, double t) {
-    if (a == null && b == null) {
-      return null;
+    if (identical(a, b)) {
+      return a;
     }
     if (a == null) {
       return FractionalOffset(ui.lerpDouble(0.5, b!.dx, t)!, ui.lerpDouble(0.5, b.dy, t)!);
diff --git a/framework/lib/src/painting/gradient.dart b/framework/lib/src/painting/gradient.dart
index 810eca6..cc63874 100644
--- a/framework/lib/src/painting/gradient.dart
+++ b/framework/lib/src/painting/gradient.dart
@@ -307,6 +307,9 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static Gradient? lerp(Gradient? a, Gradient? b, double t) {
+    if (identical(a, b)) {
+      return a;
+    }
     Gradient? result;
     if (b != null) {
       result = b.lerpFrom(a, t); // if a is null, this must return non-null
@@ -317,9 +320,6 @@
     if (result != null) {
       return result;
     }
-    if (a == null && b == null) {
-      return null;
-    }
     assert(a != null && b != null);
     return t < 0.5 ? a!.scale(1.0 - (t * 2.0)) : b!.scale((t - 0.5) * 2.0);
   }
@@ -486,8 +486,8 @@
   /// Values for `t` are usually obtained from an [Animation<double>], such as
   /// an [AnimationController].
   static LinearGradient? lerp(LinearGradient? a, LinearGradient? b, double t) {
-    if (a == null && b == null) {
-      return null;
+    if (identical(a, b)) {
+      return a;
     }
     if (a == null) {
       return b!.scale(t);
@@ -765,8 +765,8 @@
   /// Values for `t` are usually obtained from an [Animation<double>], such as
   /// an [AnimationController].
   static RadialGradient? lerp(RadialGradient? a, RadialGradient? b, double t) {
-    if (a == null && b == null) {
-      return null;
+    if (identical(a, b)) {
+      return a;
     }
     if (a == null) {
       return b!.scale(t);
@@ -1032,8 +1032,8 @@
   /// Values for `t` are usually obtained from an [Animation<double>], such as
   /// an [AnimationController].
   static SweepGradient? lerp(SweepGradient? a, SweepGradient? b, double t) {
-    if (a == null && b == null) {
-      return null;
+    if (identical(a, b)) {
+      return a;
     }
     if (a == null) {
       return b!.scale(t);
diff --git a/framework/lib/src/painting/image_provider.dart b/framework/lib/src/painting/image_provider.dart
index fbc9ccc..19cdbc4 100644
--- a/framework/lib/src/painting/image_provider.dart
+++ b/framework/lib/src/painting/image_provider.dart
@@ -4,6 +4,7 @@
 
 import 'dart:async';
 import 'dart:io';
+import 'dart:math' as math;
 import 'package:engine/ui.dart' as ui;
 import 'package:engine/ui.dart' show Locale, Size, TextDirection;
 
@@ -846,11 +847,13 @@
 class ResizeImageKey {
   // Private constructor so nobody from the outside can poison the image cache
   // with this key. It's only accessible to [ResizeImage] internally.
-  const ResizeImageKey._(this._providerCacheKey, this._width, this._height);
+  const ResizeImageKey._(this._providerCacheKey, this._policy, this._width, this._height, this._allowUpscaling);
 
   final Object _providerCacheKey;
+  final ResizeImagePolicy _policy;
   final int? _width;
   final int? _height;
+  final bool _allowUpscaling;
 
   @override
   bool operator ==(Object other) {
@@ -859,12 +862,407 @@
     }
     return other is ResizeImageKey
         && other._providerCacheKey == _providerCacheKey
+        && other._policy == _policy
         && other._width == _width
-        && other._height == _height;
+        && other._height == _height
+        && other._allowUpscaling == _allowUpscaling;
   }
 
   @override
-  int get hashCode => Object.hash(_providerCacheKey, _width, _height);
+  int get hashCode => Object.hash(_providerCacheKey, _policy, _width, _height, _allowUpscaling);
+}
+
+/// Configures the behavior for [ResizeImage].
+///
+/// This is used in [ResizeImage.policy] to affect how the [ResizeImage.width]
+/// and [ResizeImage.height] properties are interpreted.
+enum ResizeImagePolicy {
+  /// Sizes the image to the exact width and height specified by
+  /// [ResizeImage.width] and [ResizeImage.height].
+  ///
+  /// If [ResizeImage.width] and [ResizeImage.height] are both non-null, the
+  /// output image will have the specified width and height (with the
+  /// corresponding aspect ratio) regardless of whether it matches the source
+  /// image's intrinsic aspect ratio. This case is similar to [BoxFit.fill].
+  ///
+  /// If only one of `width` and `height` is non-null, then the output image
+  /// will be scaled to the associated width or height, and the other dimension
+  /// will take whatever value is needed to maintain the image's original aspect
+  /// ratio. These cases are simnilar to [BoxFit.fitWidth] and
+  /// [BoxFit.fitHeight], respectively.
+  ///
+  /// If [ResizeImage.allowUpscaling] is false (the default), the width and the
+  /// height of the output image will each be clamped to the intrinsic width and
+  /// height of the image. This may result in a different aspect ratio than the
+  /// aspect ratio specified by the target width and height (e.g. if the height
+  /// gets clamped downwards but the width does not).
+  ///
+  /// ## Examples
+  ///
+  /// The examples below show how [ResizeImagePolicy.exact] works in various
+  /// scenarios. In each example, the source image has a size of 300x200
+  /// (landscape orientation), the red box is a 150x150 square, and the green
+  /// box is a 400x400 square.
+  ///
+  /// <table>
+  /// <tr>
+  /// <td>Scenario</td>
+  /// <td>Output</td>
+  /// </tr>
+  /// <tr>
+  /// <td>
+  ///
+  /// ```dart
+  /// const ResizeImage(
+  ///   AssetImage('dragon_cake.jpg'),
+  ///   width: 150,
+  ///   height: 150,
+  /// )
+  /// ```
+  ///
+  /// </td>
+  /// <td>
+  ///
+  /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_exact_150x150_false.png)
+  ///
+  /// </td>
+  /// </tr>
+  /// <tr>
+  /// <td>
+  ///
+  /// ```dart
+  /// const ResizeImage(
+  ///   AssetImage('dragon_cake.jpg'),
+  ///   width: 150,
+  /// )
+  /// ```
+  ///
+  /// </td>
+  /// <td>
+  ///
+  /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_exact_150xnull_false.png)
+  ///
+  /// </td>
+  /// </tr>
+  /// <tr>
+  /// <td>
+  ///
+  /// ```dart
+  /// const ResizeImage(
+  ///   AssetImage('dragon_cake.jpg'),
+  ///   height: 150,
+  /// )
+  /// ```
+  ///
+  /// </td>
+  /// <td>
+  ///
+  /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_exact_nullx150_false.png)
+  ///
+  /// </td>
+  /// </tr>
+  /// <tr>
+  /// <td>
+  ///
+  /// ```dart
+  /// const ResizeImage(
+  ///   AssetImage('dragon_cake.jpg'),
+  ///   width: 400,
+  ///   height: 400,
+  /// )
+  /// ```
+  ///
+  /// </td>
+  /// <td>
+  ///
+  /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_exact_400x400_false.png)
+  ///
+  /// </td>
+  /// </tr>
+  /// <tr>
+  /// <td>
+  ///
+  /// ```dart
+  /// const ResizeImage(
+  ///   AssetImage('dragon_cake.jpg'),
+  ///   width: 400,
+  /// )
+  /// ```
+  ///
+  /// </td>
+  /// <td>
+  ///
+  /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_exact_400xnull_false.png)
+  ///
+  /// </td>
+  /// </tr>
+  /// <tr>
+  /// <td>
+  ///
+  /// ```dart
+  /// const ResizeImage(
+  ///   AssetImage('dragon_cake.jpg'),
+  ///   height: 400,
+  /// )
+  /// ```
+  ///
+  /// </td>
+  /// <td>
+  ///
+  /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_exact_nullx400_false.png)
+  ///
+  /// </td>
+  /// </tr>
+  /// <tr>
+  /// <td>
+  ///
+  /// ```dart
+  /// const ResizeImage(
+  ///   AssetImage('dragon_cake.jpg'),
+  ///   width: 400,
+  ///   height: 400,
+  ///   allowUpscaling: true,
+  /// )
+  /// ```
+  ///
+  /// </td>
+  /// <td>
+  ///
+  /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_exact_400x400_true.png)
+  ///
+  /// </td>
+  /// </tr>
+  /// <tr>
+  /// <td>
+  ///
+  /// ```dart
+  /// const ResizeImage(
+  ///   AssetImage('dragon_cake.jpg'),
+  ///   width: 400,
+  ///   allowUpscaling: true,
+  /// )
+  /// ```
+  ///
+  /// </td>
+  /// <td>
+  ///
+  /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_exact_400xnull_true.png)
+  ///
+  /// </td>
+  /// </tr>
+  /// <tr>
+  /// <td>
+  ///
+  /// ```dart
+  /// const ResizeImage(
+  ///   AssetImage('dragon_cake.jpg'),
+  ///   height: 400,
+  ///   allowUpscaling: true,
+  /// )
+  /// ```
+  ///
+  /// </td>
+  /// <td>
+  ///
+  /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_exact_nullx400_true.png)
+  ///
+  /// </td>
+  /// </tr>
+  /// </table>
+  exact,
+
+  /// Scales the image as necessary to ensure that it fits within the bounding
+  /// box specified by [ResizeImage.width] and [ResizeImage.height] while
+  /// maintaining its aspect ratio.
+  ///
+  /// If [ResizeImage.allowUpscaling] is true, the image will be scaled up or
+  /// down to best fit the bounding box; otherwise it will only ever be scaled
+  /// down.
+  ///
+  /// This is conceptually similar to [BoxFit.contain].
+  ///
+  /// ## Examples
+  ///
+  /// The examples below show how [ResizeImagePolicy.fit] works in various
+  /// scenarios. In each example, the source image has a size of 300x200
+  /// (landscape orientation), the red box is a 150x150 square, and the green
+  /// box is a 400x400 square.
+  ///
+  /// <table>
+  /// <tr>
+  /// <td>Scenario</td>
+  /// <td>Output</td>
+  /// </tr>
+  /// <tr>
+  /// <td>
+  ///
+  /// ```dart
+  /// const ResizeImage(
+  ///   AssetImage('dragon_cake.jpg'),
+  ///   policy: ResizeImagePolicy.fit,
+  ///   width: 150,
+  ///   height: 150,
+  /// )
+  /// ```
+  ///
+  /// </td>
+  /// <td>
+  ///
+  /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_fit_150x150_false.png)
+  ///
+  /// </td>
+  /// </tr>
+  /// <tr>
+  /// <td>
+  ///
+  /// ```dart
+  /// const ResizeImage(
+  ///   AssetImage('dragon_cake.jpg'),
+  ///   policy: ResizeImagePolicy.fit,
+  ///   width: 150,
+  /// )
+  /// ```
+  ///
+  /// </td>
+  /// <td>
+  ///
+  /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_fit_150xnull_false.png)
+  ///
+  /// </td>
+  /// </tr>
+  /// <tr>
+  /// <td>
+  ///
+  /// ```dart
+  /// const ResizeImage(
+  ///   AssetImage('dragon_cake.jpg'),
+  ///   policy: ResizeImagePolicy.fit,
+  ///   height: 150,
+  /// )
+  /// ```
+  ///
+  /// </td>
+  /// <td>
+  ///
+  /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_fit_nullx150_false.png)
+  ///
+  /// </td>
+  /// </tr>
+  /// <tr>
+  /// <td>
+  ///
+  /// ```dart
+  /// const ResizeImage(
+  ///   AssetImage('dragon_cake.jpg'),
+  ///   policy: ResizeImagePolicy.fit,
+  ///   width: 400,
+  ///   height: 400,
+  /// )
+  /// ```
+  ///
+  /// </td>
+  /// <td>
+  ///
+  /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_fit_400x400_false.png)
+  ///
+  /// </td>
+  /// </tr>
+  /// <tr>
+  /// <td>
+  ///
+  /// ```dart
+  /// const ResizeImage(
+  ///   AssetImage('dragon_cake.jpg'),
+  ///   policy: ResizeImagePolicy.fit,
+  ///   width: 400,
+  /// )
+  /// ```
+  ///
+  /// </td>
+  /// <td>
+  ///
+  /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_fit_400xnull_false.png)
+  ///
+  /// </td>
+  /// </tr>
+  /// <tr>
+  /// <td>
+  ///
+  /// ```dart
+  /// const ResizeImage(
+  ///   AssetImage('dragon_cake.jpg'),
+  ///   policy: ResizeImagePolicy.fit,
+  ///   height: 400,
+  /// )
+  /// ```
+  ///
+  /// </td>
+  /// <td>
+  ///
+  /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_fit_nullx400_false.png)
+  ///
+  /// </td>
+  /// </tr>
+  /// <tr>
+  /// <td>
+  ///
+  /// ```dart
+  /// const ResizeImage(
+  ///   AssetImage('dragon_cake.jpg'),
+  ///   policy: ResizeImagePolicy.fit,
+  ///   width: 400,
+  ///   height: 400,
+  ///   allowUpscaling: true,
+  /// )
+  /// ```
+  ///
+  /// </td>
+  /// <td>
+  ///
+  /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_fit_400x400_true.png)
+  ///
+  /// </td>
+  /// </tr>
+  /// <tr>
+  /// <td>
+  ///
+  /// ```dart
+  /// const ResizeImage(
+  ///   AssetImage('dragon_cake.jpg'),
+  ///   policy: ResizeImagePolicy.fit,
+  ///   width: 400,
+  ///   allowUpscaling: true,
+  /// )
+  /// ```
+  ///
+  /// </td>
+  /// <td>
+  ///
+  /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_fit_400xnull_true.png)
+  ///
+  /// </td>
+  /// </tr>
+  /// <tr>
+  /// <td>
+  ///
+  /// ```dart
+  /// const ResizeImage(
+  ///   AssetImage('dragon_cake.jpg'),
+  ///   policy: ResizeImagePolicy.fit,
+  ///   height: 400,
+  ///   allowUpscaling: true,
+  /// )
+  /// ```
+  ///
+  /// </td>
+  /// <td>
+  ///
+  /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_fit_nullx400_true.png)
+  ///
+  /// </td>
+  /// </tr>
+  /// </table>
+  fit,
 }
 
 /// Instructs Flutter to decode the image at the specified dimensions
@@ -881,10 +1279,13 @@
   /// The cached image will be directly decoded and stored at the resolution
   /// defined by `width` and `height`. The image will lose detail and
   /// use less memory if resized to a size smaller than the native size.
+  ///
+  /// At least one of `width` and `height` must be non-null.
   const ResizeImage(
     this.imageProvider, {
     this.width,
     this.height,
+    this.policy = ResizeImagePolicy.exact,
     this.allowUpscaling = false,
   }) : assert(width != null || height != null);
 
@@ -892,11 +1293,20 @@
   final ImageProvider imageProvider;
 
   /// The width the image should decode to and cache.
+  ///
+  /// At least one of this and [height] must be non-null.
   final int? width;
 
   /// The height the image should decode to and cache.
+  ///
+  /// At least one of this and [width] must be non-null.
   final int? height;
 
+  /// The policy that determines how [width] and [height] are interpreted.
+  ///
+  /// Defaults to [ResizeImagePolicy.exact].
+  final ResizeImagePolicy policy;
+
   /// Whether the [width] and [height] parameters should be clamped to the
   /// intrinsic width and height of the image.
   ///
@@ -919,6 +1329,10 @@
   }
 
   @override
+  @Deprecated(
+    'Implement loadImage for faster image loading. '
+    'This feature was deprecated after v2.13.0-1.0.pre.',
+  )
   ImageStreamCompleter load(ResizeImageKey key, DecoderCallback decode) {
     Future<ui.Codec> decodeResize(Uint8List buffer, {int? cacheWidth, int? cacheHeight, bool? allowUpscaling}) {
       assert(
@@ -936,6 +1350,10 @@
   }
 
   @override
+  @Deprecated(
+    'Implement loadImage for image loading. '
+    'This feature was deprecated after v3.7.0-1.4.pre.',
+  )
   ImageStreamCompleter loadBuffer(ResizeImageKey key, DecoderBufferCallback decode) {
     Future<ui.Codec> decodeResize(ui.ImmutableBuffer buffer, {int? cacheWidth, int? cacheHeight, bool? allowUpscaling}) {
       assert(
@@ -962,17 +1380,56 @@
         'getTargetSize.',
       );
       return decode(buffer, getTargetSize: (int intrinsicWidth, int intrinsicHeight) {
-        int? targetWidth = width;
-        int? targetHeight = height;
-        if (!allowUpscaling) {
-          if (targetWidth != null && targetWidth > intrinsicWidth) {
-            targetWidth = intrinsicWidth;
-          }
-          if (targetHeight != null && targetHeight > intrinsicHeight) {
-            targetHeight = intrinsicHeight;
-          }
+        switch (policy) {
+          case ResizeImagePolicy.exact:
+            int? targetWidth = width;
+            int? targetHeight = height;
+
+            if (!allowUpscaling) {
+              if (targetWidth != null && targetWidth > intrinsicWidth) {
+                targetWidth = intrinsicWidth;
+              }
+              if (targetHeight != null && targetHeight > intrinsicHeight) {
+                targetHeight = intrinsicHeight;
+              }
+            }
+
+            return ui.TargetImageSize(width: targetWidth, height: targetHeight);
+          case ResizeImagePolicy.fit:
+            final double aspectRatio = intrinsicWidth / intrinsicHeight;
+            final int maxWidth = width ?? intrinsicWidth;
+            final int maxHeight = height ?? intrinsicHeight;
+            int targetWidth = intrinsicWidth;
+            int targetHeight = intrinsicHeight;
+
+            if (targetWidth > maxWidth) {
+              targetWidth = maxWidth;
+              targetHeight = targetWidth ~/ aspectRatio;
+            }
+
+            if (targetHeight > maxHeight) {
+              targetHeight = maxHeight;
+              targetWidth = (targetHeight * aspectRatio).floor();
+            }
+
+            if (allowUpscaling) {
+              if (width == null) {
+                assert(height != null);
+                targetHeight = height!;
+                targetWidth = (targetHeight * aspectRatio).floor();
+              } else if (height == null) {
+                targetWidth = width!;
+                targetHeight = targetWidth ~/ aspectRatio;
+              } else {
+                final int derivedMaxWidth = (maxHeight * aspectRatio).floor();
+                final int derivedMaxHeight = maxWidth ~/ aspectRatio;
+                targetWidth = math.min(maxWidth, derivedMaxWidth);
+                targetHeight = math.min(maxHeight, derivedMaxHeight);
+              }
+            }
+
+            return ui.TargetImageSize(width: targetWidth, height: targetHeight);
         }
-        return ui.TargetImageSize(width: targetWidth, height: targetHeight);
       });
     }
 
@@ -993,10 +1450,10 @@
       if (completer == null) {
         // This future has completed synchronously (completer was never assigned),
         // so we can directly create the synchronous result to return.
-        result = SynchronousFuture<ResizeImageKey>(ResizeImageKey._(key, width, height));
+        result = SynchronousFuture<ResizeImageKey>(ResizeImageKey._(key, policy, width, height, allowUpscaling));
       } else {
         // This future did not synchronously complete.
-        completer.complete(ResizeImageKey._(key, width, height));
+        completer.complete(ResizeImageKey._(key, policy, width, height, allowUpscaling));
       }
     });
     if (result != null) {
diff --git a/framework/lib/src/painting/image_resolution.dart b/framework/lib/src/painting/image_resolution.dart
index b7e88ee..50d95c5 100644
--- a/framework/lib/src/painting/image_resolution.dart
+++ b/framework/lib/src/painting/image_resolution.dart
@@ -4,15 +4,12 @@
 
 import 'dart:async';
 import 'dart:collection';
-import 'dart:convert';
 
 import 'package:flute/foundation.dart';
 import 'package:flute/services.dart';
 
 import 'image_provider.dart';
 
-const String _kAssetManifestFileName = 'AssetManifest.json';
-
 /// A screen with a device-pixel ratio strictly less than this value is
 /// considered a low-resolution screen (typically entry-level to mid-range
 /// laptops, desktop screens up to QHD, low-end tablets such as Kindle Fire).
@@ -284,18 +281,18 @@
     Completer<AssetBundleImageKey>? completer;
     Future<AssetBundleImageKey>? result;
 
-    chosenBundle.loadStructuredData<Map<String, List<String>>?>(_kAssetManifestFileName, manifestParser).then<void>(
-      (Map<String, List<String>>? manifest) {
-        final String chosenName = _chooseVariant(
+    AssetManifest.loadFromAssetBundle(chosenBundle)
+      .then((AssetManifest manifest) {
+        final Iterable<AssetMetadata>? candidateVariants = manifest.getAssetVariants(keyName);
+        final AssetMetadata chosenVariant = _chooseVariant(
           keyName,
           configuration,
-          manifest == null ? null : manifest[keyName],
-        )!;
-        final double chosenScale = _parseScale(chosenName);
+          candidateVariants,
+        );
         final AssetBundleImageKey key = AssetBundleImageKey(
           bundle: chosenBundle,
-          name: chosenName,
-          scale: chosenScale,
+          name: chosenVariant.key,
+          scale: chosenVariant.targetDevicePixelRatio ?? _naturalResolution,
         );
         if (completer != null) {
           // We already returned from this function, which means we are in the
@@ -309,14 +306,15 @@
           // ourselves.
           result = SynchronousFuture<AssetBundleImageKey>(key);
         }
-      },
-    ).catchError((Object error, StackTrace stack) {
-      // We had an error. (This guarantees we weren't called synchronously.)
-      // Forward the error to the caller.
-      assert(completer != null);
-      assert(result == null);
-      completer!.completeError(error, stack);
-    });
+      })
+      .onError((Object error, StackTrace stack) {
+        // We had an error. (This guarantees we weren't called synchronously.)
+        // Forward the error to the caller.
+        assert(completer != null);
+        assert(result == null);
+        completer!.completeError(error, stack);
+      });
+
     if (result != null) {
       // The code above ran synchronously, and came up with an answer.
       // Return the SynchronousFuture that we created above.
@@ -328,35 +326,24 @@
     return completer.future;
   }
 
-  /// Parses the asset manifest string into a strongly-typed map.
-  @visibleForTesting
-  static Future<Map<String, List<String>>?> manifestParser(String? jsonData) {
-    if (jsonData == null) {
-      return SynchronousFuture<Map<String, List<String>>?>(null);
+  AssetMetadata _chooseVariant(String mainAssetKey, ImageConfiguration config, Iterable<AssetMetadata>? candidateVariants) {
+    if (candidateVariants == null) {
+      return AssetMetadata(key: mainAssetKey, targetDevicePixelRatio: null, main: true);
     }
-    // TODO(ianh): JSON decoding really shouldn't be on the main thread.
-    final Map<String, dynamic> parsedJson = json.decode(jsonData) as Map<String, dynamic>;
-    final Iterable<String> keys = parsedJson.keys;
-    final Map<String, List<String>> parsedManifest = <String, List<String>> {
-      for (final String key in keys) key: List<String>.from(parsedJson[key] as List<dynamic>),
-    };
-    // TODO(ianh): convert that data structure to the right types.
-    return SynchronousFuture<Map<String, List<String>>?>(parsedManifest);
-  }
 
-  String? _chooseVariant(String main, ImageConfiguration config, List<String>? candidates) {
-    if (config.devicePixelRatio == null || candidates == null || candidates.isEmpty) {
-      return main;
+    if (config.devicePixelRatio == null) {
+      return candidateVariants.firstWhere((AssetMetadata variant) => variant.main);
     }
-    // TODO(ianh): Consider moving this parsing logic into _manifestParser.
-    final SplayTreeMap<double, String> mapping = SplayTreeMap<double, String>();
-    for (final String candidate in candidates) {
-      mapping[_parseScale(candidate)] = candidate;
+
+    final SplayTreeMap<double, AssetMetadata> candidatesByDevicePixelRatio =
+      SplayTreeMap<double, AssetMetadata>();
+    for (final AssetMetadata candidate in candidateVariants) {
+      candidatesByDevicePixelRatio[candidate.targetDevicePixelRatio ?? _naturalResolution] = candidate;
     }
     // TODO(ianh): implement support for config.locale, config.textDirection,
     // config.size, config.platform (then document this over in the Image.asset
     // docs)
-    return _findBestVariant(mapping, config.devicePixelRatio!);
+    return _findBestVariant(candidatesByDevicePixelRatio, config.devicePixelRatio!);
   }
 
   // Returns the "best" asset variant amongst the available `candidates`.
@@ -371,17 +358,17 @@
   //   lowest key higher than `value`.
   // - If the screen has high device pixel ratio, choose the variant with the
   //   key nearest to `value`.
-  String? _findBestVariant(SplayTreeMap<double, String> candidates, double value) {
-    if (candidates.containsKey(value)) {
-      return candidates[value]!;
+  AssetMetadata _findBestVariant(SplayTreeMap<double, AssetMetadata> candidatesByDpr, double value) {
+    if (candidatesByDpr.containsKey(value)) {
+      return candidatesByDpr[value]!;
     }
-    final double? lower = candidates.lastKeyBefore(value);
-    final double? upper = candidates.firstKeyAfter(value);
+    final double? lower = candidatesByDpr.lastKeyBefore(value);
+    final double? upper = candidatesByDpr.firstKeyAfter(value);
     if (lower == null) {
-      return candidates[upper];
+      return candidatesByDpr[upper]!;
     }
     if (upper == null) {
-      return candidates[lower];
+      return candidatesByDpr[lower]!;
     }
 
     // On screens with low device-pixel ratios the artifacts from upscaling
@@ -389,32 +376,12 @@
     // ratios because the physical pixels are larger. Choose the higher
     // resolution image in that case instead of the nearest one.
     if (value < _kLowDprLimit || value > (lower + upper) / 2) {
-      return candidates[upper];
+      return candidatesByDpr[upper]!;
     } else {
-      return candidates[lower];
+      return candidatesByDpr[lower]!;
     }
   }
 
-  static final RegExp _extractRatioRegExp = RegExp(r'/?(\d+(\.\d*)?)x$');
-
-  double _parseScale(String key) {
-    if (key == assetName) {
-      return _naturalResolution;
-    }
-
-    final Uri assetUri = Uri.parse(key);
-    String directoryPath = '';
-    if (assetUri.pathSegments.length > 1) {
-      directoryPath = assetUri.pathSegments[assetUri.pathSegments.length - 2];
-    }
-
-    final Match? match = _extractRatioRegExp.firstMatch(directoryPath);
-    if (match != null && match.groupCount > 0) {
-      return double.parse(match.group(1)!);
-    }
-    return _naturalResolution; // i.e. default to 1.0x
-  }
-
   @override
   bool operator ==(Object other) {
     if (other.runtimeType != runtimeType) {
diff --git a/framework/lib/src/painting/linear_border.dart b/framework/lib/src/painting/linear_border.dart
index 6fbd234..89045e0 100644
--- a/framework/lib/src/painting/linear_border.dart
+++ b/framework/lib/src/painting/linear_border.dart
@@ -59,8 +59,8 @@
   /// is null then we interpolate from `a` varying size from `a.size` to zero.
   /// Otherwise both values are interpolated.
   static LinearBorderEdge? lerp(LinearBorderEdge? a, LinearBorderEdge? b, double t) {
-    if (a == null && b == null) {
-      return null;
+    if (identical(a, b)) {
+      return a;
     }
 
     a ??= LinearBorderEdge(alignment: b!.alignment, size: 0);
diff --git a/framework/lib/src/painting/shape_decoration.dart b/framework/lib/src/painting/shape_decoration.dart
index 43fc655..793922c 100644
--- a/framework/lib/src/painting/shape_decoration.dart
+++ b/framework/lib/src/painting/shape_decoration.dart
@@ -225,8 +225,8 @@
   ///    and which use [ShapeDecoration.lerp] when interpolating two
   ///    [ShapeDecoration]s or a [ShapeDecoration] to or from null.
   static ShapeDecoration? lerp(ShapeDecoration? a, ShapeDecoration? b, double t) {
-    if (a == null && b == null) {
-      return null;
+    if (identical(a, b)) {
+      return a;
     }
     if (a != null && b != null) {
       if (t == 0.0) {
diff --git a/framework/lib/src/painting/text_style.dart b/framework/lib/src/painting/text_style.dart
index 2b390a5..a530338 100644
--- a/framework/lib/src/painting/text_style.dart
+++ b/framework/lib/src/painting/text_style.dart
@@ -1077,7 +1077,7 @@
   /// implementation uses the non-null value throughout the transition for
   /// lerpable fields such as colors (for example, if one [TextStyle] specified
   /// `fontSize` but the other didn't, the returned [TextStyle] will use the
-  /// `fontSize` from the [TextStyle] that specified it, regarless of the `t`
+  /// `fontSize` from the [TextStyle] that specified it, regardless of the `t`
   /// value).
   ///
   /// This method throws when the given [TextStyle]s don't have the same
@@ -1094,10 +1094,9 @@
   /// as if they have a [background] paint (creating a new [Paint] if necessary
   /// based on the [backgroundColor] property).
   static TextStyle? lerp(TextStyle? a, TextStyle? b, double t) {
-    if (a == null && b == null) {
-      return null;
+    if (identical(a, b)) {
+      return a;
     }
-
     String? lerpDebugLabel;
     assert(() {
       lerpDebugLabel = 'lerp(${a?.debugLabel ?? _kDefaultDebugLabel} ⎯${t.toStringAsFixed(1)}→ ${b?.debugLabel ?? _kDefaultDebugLabel})';
diff --git a/framework/lib/src/physics/spring_simulation.dart b/framework/lib/src/physics/spring_simulation.dart
index dbd8153..f756ae5 100644
--- a/framework/lib/src/physics/spring_simulation.dart
+++ b/framework/lib/src/physics/spring_simulation.dart
@@ -176,7 +176,7 @@
   ) {
     final double r = -spring.damping / (2.0 * spring.mass);
     final double c1 = distance;
-    final double c2 = velocity / (r * distance);
+    final double c2 = velocity - (r * distance);
     return _CriticalSolution.withArgs(r, c1, c2);
   }
 
diff --git a/framework/lib/src/rendering/binding.dart b/framework/lib/src/rendering/binding.dart
index da8c459..3e022aa 100644
--- a/framework/lib/src/rendering/binding.dart
+++ b/framework/lib/src/rendering/binding.dart
@@ -38,16 +38,15 @@
     platformDispatcher
       ..onMetricsChanged = handleMetricsChanged
       ..onTextScaleFactorChanged = handleTextScaleFactorChanged
-      ..onPlatformBrightnessChanged = handlePlatformBrightnessChanged
-      ..onSemanticsEnabledChanged = _handleSemanticsEnabledChanged
-      ..onSemanticsAction = _handleSemanticsAction;
+      ..onPlatformBrightnessChanged = handlePlatformBrightnessChanged;
     initRenderView();
-    _handleSemanticsEnabledChanged();
     addPersistentFrameCallback(_handlePersistentFrameCallback);
     initMouseTracker();
     if (kIsWeb) {
       addPostFrameCallback(_handleWebFirstFrame);
     }
+    addSemanticsEnabledListener(_handleSemanticsEnabledChanged);
+    _handleSemanticsEnabledChanged();
   }
 
   /// The current [RendererBinding], if one has been created.
@@ -308,8 +307,6 @@
     );
   }
 
-  SemanticsHandle? _semanticsHandle;
-
   /// Creates a [MouseTracker] which manages state about currently connected
   /// mice, for hover notification.
   ///
@@ -333,14 +330,10 @@
     super.dispatchEvent(event, hitTestResult);
   }
 
-  void _handleSemanticsEnabledChanged() {
-    setSemanticsEnabled(platformDispatcher.semanticsEnabled);
-  }
+  SemanticsHandle? _semanticsHandle;
 
-  /// Whether the render tree associated with this binding should produce a tree
-  /// of [SemanticsNode] objects.
-  void setSemanticsEnabled(bool enabled) {
-    if (enabled) {
+  void _handleSemanticsEnabledChanged() {
+    if (semanticsEnabled) {
       _semanticsHandle ??= _pipelineOwner.ensureSemantics();
     } else {
       _semanticsHandle?.dispose();
@@ -348,18 +341,9 @@
     }
   }
 
-  void _handleWebFirstFrame(Duration _) {
-    assert(kIsWeb);
-    const MethodChannel methodChannel = MethodChannel('flutter/service_worker');
-    methodChannel.invokeMethod<void>('first-frame');
-  }
-
-  void _handleSemanticsAction(int id, SemanticsAction action, ByteData? args) {
-    _pipelineOwner.semanticsOwner?.performAction(
-      id,
-      action,
-      args != null ? const StandardMessageCodec().decodeMessage(args) : null,
-    );
+  @override
+  void performSemanticsAction(SemanticsActionEvent action) {
+    _pipelineOwner.semanticsOwner?.performAction(action.nodeId, action.type, action.arguments);
   }
 
   void _handleSemanticsOwnerCreated() {
@@ -374,6 +358,12 @@
     renderView.clearSemantics();
   }
 
+  void _handleWebFirstFrame(Duration _) {
+    assert(kIsWeb);
+    const MethodChannel methodChannel = MethodChannel('flutter/service_worker');
+    methodChannel.invokeMethod<void>('first-frame');
+  }
+
   void _handlePersistentFrameCallback(Duration timeStamp) {
     drawFrame();
     _scheduleMouseTrackerUpdate();
diff --git a/framework/lib/src/rendering/box.dart b/framework/lib/src/rendering/box.dart
index 057a831..1a0a020 100644
--- a/framework/lib/src/rendering/box.dart
+++ b/framework/lib/src/rendering/box.dart
@@ -468,8 +468,8 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static BoxConstraints? lerp(BoxConstraints? a, BoxConstraints? b, double t) {
-    if (a == null && b == null) {
-      return null;
+    if (identical(a, b)) {
+      return a;
     }
     if (a == null) {
       return b! * t;
diff --git a/framework/lib/src/rendering/editable.dart b/framework/lib/src/rendering/editable.dart
index 407b88c..624761c 100644
--- a/framework/lib/src/rendering/editable.dart
+++ b/framework/lib/src/rendering/editable.dart
@@ -729,7 +729,7 @@
     // Check if the selection is visible with an approximation because a
     // difference between rounded and unrounded values causes the caret to be
     // reported as having a slightly (< 0.5) negative y offset. This rounding
-    // happens in paragraph.cc's layout and TextPainer's
+    // happens in paragraph.cc's layout and TextPainter's
     // _applyFloatingPointHack. Ideally, the rounding mismatch will be fixed and
     // this can be changed to be a strict check instead of an approximation.
     const double visibleRegionSlop = 0.5;
@@ -775,7 +775,7 @@
   @override
   void markNeedsPaint() {
     super.markNeedsPaint();
-    // Tell the painers to repaint since text layout may have changed.
+    // Tell the painters to repaint since text layout may have changed.
     _foregroundRenderObject?.markNeedsPaint();
     _backgroundRenderObject?.markNeedsPaint();
   }
diff --git a/framework/lib/src/rendering/object.dart b/framework/lib/src/rendering/object.dart
index c1f1159..d48d600 100644
--- a/framework/lib/src/rendering/object.dart
+++ b/framework/lib/src/rendering/object.dart
@@ -15,6 +15,7 @@
 
 import 'debug.dart';
 import 'layer.dart';
+import 'proxy_box.dart';
 
 export 'package:flute/foundation.dart' show
   DiagnosticPropertiesBuilder,
@@ -807,24 +808,8 @@
 /// Used by [RenderObject.invokeLayoutCallback].
 typedef LayoutCallback<T extends Constraints> = void Function(T constraints);
 
-/// A reference to the semantics tree.
-///
-/// The framework maintains the semantics tree (used for accessibility and
-/// indexing) only when there is at least one client holding an open
-/// [SemanticsHandle].
-///
-/// The framework notifies the client that it has updated the semantics tree by
-/// calling the [listener] callback. When the client no longer needs the
-/// semantics tree, the client can call [dispose] on the [SemanticsHandle],
-/// which stops these callbacks and closes the [SemanticsHandle]. When all the
-/// outstanding [SemanticsHandle] objects are closed, the framework stops
-/// updating the semantics tree.
-///
-/// To obtain a [SemanticsHandle], call [PipelineOwner.ensureSemantics] on the
-/// [PipelineOwner] for the render tree from which you wish to read semantics.
-/// You can obtain the [PipelineOwner] using the [RenderObject.owner] property.
-class SemanticsHandle {
-  SemanticsHandle._(PipelineOwner owner, this.listener)
+class _LocalSemanticsHandle implements SemanticsHandle {
+  _LocalSemanticsHandle._(PipelineOwner owner, this.listener)
       : _owner = owner {
     if (listener != null) {
       _owner.semanticsOwner!.addListener(listener!);
@@ -836,13 +821,7 @@
   /// The callback that will be notified when the semantics tree updates.
   final VoidCallback? listener;
 
-  /// Closes the semantics handle and stops calling [listener] when the
-  /// semantics updates.
-  ///
-  /// When all the outstanding [SemanticsHandle] objects for a given
-  /// [PipelineOwner] are closed, the [PipelineOwner] will stop updating the
-  /// semantics tree.
-  @mustCallSuper
+  @override
   void dispose() {
     if (listener != null) {
       _owner.semanticsOwner!.removeListener(listener!);
@@ -1170,7 +1149,12 @@
   int _outstandingSemanticsHandles = 0;
 
   /// Opens a [SemanticsHandle] and calls [listener] whenever the semantics tree
-  /// updates.
+  /// generated from the render tree owned by this [PipelineOwner] updates.
+  ///
+  /// Calling this method only ensures that this particular [PipelineOwner] will
+  /// generate a semantics tree. Consider calling
+  /// [SemanticsBinding.ensureSemantics] instead to turn on semantics globally
+  /// for the entire app.
   ///
   /// The [PipelineOwner] updates the semantics tree only when there are clients
   /// that wish to use the semantics tree. These clients express their interest
@@ -1189,7 +1173,7 @@
       _semanticsOwner = SemanticsOwner(onSemanticsUpdate: onSemanticsUpdate!);
       onSemanticsOwnerCreated?.call();
     }
-    return SemanticsHandle._(this, listener);
+    return _LocalSemanticsHandle._(this, listener);
   }
 
   void _didDisposeSemanticsHandle() {
@@ -1502,7 +1486,6 @@
   /// in other cases will lead to an inconsistent tree and probably cause crashes.
   @override
   void adoptChild(RenderObject child) {
-    assert(_debugCanPerformMutations);
     setupParentData(child);
     markNeedsLayout();
     markNeedsCompositingBitsUpdate();
@@ -1516,7 +1499,6 @@
   /// in other cases will lead to an inconsistent tree and probably cause crashes.
   @override
   void dropChild(RenderObject child) {
-    assert(_debugCanPerformMutations);
     assert(child.parentData != null);
     child._cleanRelayoutBoundary();
     child.parentData!.detach();
@@ -1659,7 +1641,7 @@
         }
 
         if (!activeLayoutRoot._debugMutationsLocked) {
-          final AbstractNode? p = activeLayoutRoot.parent;
+          final AbstractNode? p = activeLayoutRoot.debugLayoutParent;
           activeLayoutRoot = p is RenderObject ? p : null;
         } else {
           // activeLayoutRoot found.
@@ -1738,6 +1720,29 @@
     return result;
   }
 
+  /// The [RenderObject] that's expected to call [layout] on this [RenderObject]
+  /// in its [performLayout] implementation.
+  ///
+  /// This method is used to implement an assert that ensures the render subtree
+  /// actively performing layout can not get accidently mutated. It's only
+  /// implemented in debug mode and always returns null in release mode.
+  ///
+  /// The default implementation returns [parent] and overriding is rarely
+  /// needed. A [RenderObject] subclass that expects its
+  /// [RenderObject.performLayout] to be called from a different [RenderObject]
+  /// that's not its [parent] should override this property to return the actual
+  /// layout parent.
+  @protected
+  RenderObject? get debugLayoutParent {
+    RenderObject? layoutParent;
+    assert(() {
+      final AbstractNode? parent = this.parent;
+      layoutParent = parent is RenderObject? ? parent : null;
+      return true;
+    }());
+    return layoutParent;
+  }
+
   @override
   PipelineOwner? get owner => super.owner as PipelineOwner?;
 
@@ -2369,7 +2374,7 @@
   /// needing to paint and needing a composited layer update, this method is only
   /// called once.
   // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/102102 revisit the
-  // contraint that the instance/type of layer cannot be changed at runtime.
+  // constraint that the instance/type of layer cannot be changed at runtime.
   OffsetLayer updateCompositedLayer({required covariant OffsetLayer? oldLayer}) {
     assert(isRepaintBoundary);
     return oldLayer ?? OffsetLayer();
@@ -3244,7 +3249,7 @@
     final SemanticsConfiguration config = _semanticsConfiguration;
     bool dropSemanticsOfPreviousSiblings = config.isBlockingSemanticsOfPreviouslyPaintedNodes;
 
-    final bool producesForkingFragment = !config.hasBeenAnnotated && !config.isSemanticBoundary;
+    bool producesForkingFragment = !config.hasBeenAnnotated && !config.isSemanticBoundary;
     final bool childrenMergeIntoParent = mergeIntoParent || config.isMergingSemanticsOfDescendants;
     final List<SemanticsConfiguration> childConfigurations = <SemanticsConfiguration>[];
     final bool explicitChildNode = config.explicitChildNodes || parent is! RenderObject;
@@ -3252,6 +3257,7 @@
     final Map<SemanticsConfiguration, _InterestingSemanticsFragment> configToFragment = <SemanticsConfiguration, _InterestingSemanticsFragment>{};
     final List<_InterestingSemanticsFragment> mergeUpFragments = <_InterestingSemanticsFragment>[];
     final List<List<_InterestingSemanticsFragment>> siblingMergeFragmentGroups = <List<_InterestingSemanticsFragment>>[];
+    final bool hasTags = config.tagsForChildren?.isNotEmpty ?? false;
     visitChildrenForSemantics((RenderObject renderChild) {
       assert(!_needsLayout);
       final _SemanticsFragment parentFragment = renderChild._getSemanticsForParent(
@@ -3267,7 +3273,9 @@
       }
       for (final _InterestingSemanticsFragment fragment in parentFragment.mergeUpFragments) {
         fragment.addAncestor(this);
-        fragment.addTags(config.tagsForChildren);
+        if (hasTags) {
+          fragment.addTags(config.tagsForChildren!);
+        }
         if (hasChildConfigurationsDelegate && fragment.config != null) {
           // This fragment need to go through delegate to determine whether it
           // merge up or not.
@@ -3283,7 +3291,9 @@
         for (final List<_InterestingSemanticsFragment> siblingMergeGroup in parentFragment.siblingMergeGroups) {
           for (final _InterestingSemanticsFragment siblingMergingFragment in siblingMergeGroup) {
             siblingMergingFragment.addAncestor(this);
-            siblingMergingFragment.addTags(config.tagsForChildren);
+            if (hasTags) {
+              siblingMergingFragment.addTags(config.tagsForChildren!);
+            }
           }
           siblingMergeFragmentGroups.add(siblingMergeGroup);
         }
@@ -3296,14 +3306,25 @@
       for (final _InterestingSemanticsFragment fragment in mergeUpFragments) {
         fragment.markAsExplicit();
       }
-    } else if (hasChildConfigurationsDelegate && childConfigurations.isNotEmpty) {
+    } else if (hasChildConfigurationsDelegate) {
       final ChildSemanticsConfigurationsResult result = config.childConfigurationsDelegate!(childConfigurations);
       mergeUpFragments.addAll(
-        result.mergeUp.map<_InterestingSemanticsFragment>((SemanticsConfiguration config) => configToFragment[config]!),
+        result.mergeUp.map<_InterestingSemanticsFragment>((SemanticsConfiguration config) {
+          final _InterestingSemanticsFragment? fragment = configToFragment[config];
+          if (fragment == null) {
+            // Parent fragment of Incomplete fragments can't be a forking
+            // fragment since they need to be merged.
+            producesForkingFragment = false;
+            return _IncompleteSemanticsFragment(config: config, owner: this);
+          }
+          return fragment;
+        }),
       );
       for (final Iterable<SemanticsConfiguration> group in result.siblingMergeGroups) {
         siblingMergeFragmentGroups.add(
-          group.map<_InterestingSemanticsFragment>((SemanticsConfiguration config) => configToFragment[config]!).toList()
+          group.map<_InterestingSemanticsFragment>((SemanticsConfiguration config) {
+            return configToFragment[config] ?? _IncompleteSemanticsFragment(config: config, owner: this);
+          }).toList(),
         );
       }
     }
@@ -3636,17 +3657,13 @@
   @override
   void attach(PipelineOwner owner) {
     super.attach(owner);
-    if (_child != null) {
-      _child!.attach(owner);
-    }
+    _child?.attach(owner);
   }
 
   @override
   void detach() {
     super.detach();
-    if (_child != null) {
-      _child!.detach();
-    }
+    _child?.detach();
   }
 
   @override
@@ -4184,10 +4201,10 @@
   Set<SemanticsTag>? _tagsForChildren;
 
   /// Tag all children produced by [compileChildren] with `tags`.
-  void addTags(Iterable<SemanticsTag>? tags) {
-    if (tags == null || tags.isEmpty) {
-      return;
-    }
+  ///
+  /// `tags` must not be empty.
+  void addTags(Iterable<SemanticsTag> tags) {
+    assert(tags.isNotEmpty);
     _tagsForChildren ??= <SemanticsTag>{};
     _tagsForChildren!.addAll(tags);
   }
@@ -4281,6 +4298,48 @@
   }
 }
 
+/// A fragment with partial information that must not form an explicit
+/// semantics node without merging into another _SwitchableSemanticsFragment.
+///
+/// This fragment is generated from synthetic SemanticsConfiguration returned from
+/// [SemanticsConfiguration.childConfigurationsDelegate].
+class _IncompleteSemanticsFragment extends _InterestingSemanticsFragment {
+  _IncompleteSemanticsFragment({
+    required this.config,
+    required super.owner,
+  }) : super(dropsSemanticsOfPreviousSiblings: false);
+
+  @override
+  void addAll(Iterable<_InterestingSemanticsFragment> fragments) {
+    assert(false, 'This fragment must be a leaf node');
+  }
+
+  @override
+  void compileChildren({
+    required Rect? parentSemanticsClipRect,
+    required Rect? parentPaintClipRect,
+    required double elevationAdjustment,
+    required List<SemanticsNode> result,
+    required List<SemanticsNode> siblingNodes,
+  }) {
+    // There is nothing to do because this fragment must be a leaf node and
+    // must not be explicit.
+  }
+
+  @override
+  final SemanticsConfiguration config;
+
+  @override
+  void markAsExplicit() {
+    assert(
+      false,
+      'SemanticsConfiguration created in '
+      'SemanticsConfiguration.childConfigurationsDelegate must not produce '
+      'its own semantics node'
+    );
+  }
+}
+
 /// An [_InterestingSemanticsFragment] that can be told to only add explicit
 /// [SemanticsNode]s to the parent.
 ///
@@ -4559,6 +4618,17 @@
     }
   }
 
+  @override
+  void addTags(Iterable<SemanticsTag> tags) {
+    super.addTags(tags);
+    // _ContainerSemanticsFragments add their tags to child fragments through
+    // this method. This fragment must make sure its _config is in sync.
+    if (tags.isNotEmpty) {
+      _ensureConfigIsWritable();
+      tags.forEach(_config.addTagForChildren);
+    }
+  }
+
   void _ensureConfigIsWritable() {
     if (!_isConfigWritable) {
       _config = _config.copy();
diff --git a/framework/lib/src/rendering/paragraph.dart b/framework/lib/src/rendering/paragraph.dart
index 5b44dc1..0ebfeb7 100644
--- a/framework/lib/src/rendering/paragraph.dart
+++ b/framework/lib/src/rendering/paragraph.dart
@@ -119,7 +119,9 @@
 
   static final String _placeholderCharacter = String.fromCharCode(PlaceholderSpan.placeholderCodeUnit);
   final TextPainter _textPainter;
-  AttributedString? _cachedAttributedLabel;
+
+  List<AttributedString>? _cachedAttributedLabels;
+
   List<InlineSpanSemanticsInformation>? _cachedCombinedSemanticsInfos;
 
   /// The text to display.
@@ -135,7 +137,7 @@
         break;
       case RenderComparison.paint:
         _textPainter.text = value;
-        _cachedAttributedLabel = null;
+        _cachedAttributedLabels = null;
         _cachedCombinedSemanticsInfos = null;
         _extractPlaceholderSpans(value);
         markNeedsPaint();
@@ -144,7 +146,7 @@
       case RenderComparison.layout:
         _textPainter.text = value;
         _overflowShader = null;
-        _cachedAttributedLabel = null;
+        _cachedAttributedLabels = null;
         _cachedCombinedSemanticsInfos = null;
         _extractPlaceholderSpans(value);
         markNeedsLayout();
@@ -1035,12 +1037,23 @@
   void describeSemanticsConfiguration(SemanticsConfiguration config) {
     super.describeSemanticsConfiguration(config);
     _semanticsInfo = text.getSemanticsInformation();
+    bool needsAssembleSemanticsNode = false;
+    bool needsChildConfigrationsDelegate = false;
+    for (final InlineSpanSemanticsInformation info in _semanticsInfo!) {
+      if (info.recognizer != null) {
+        needsAssembleSemanticsNode = true;
+        break;
+      }
+      needsChildConfigrationsDelegate = needsChildConfigrationsDelegate || info.isPlaceholder;
+    }
 
-    if (_semanticsInfo!.any((InlineSpanSemanticsInformation info) => info.recognizer != null)) {
+    if (needsAssembleSemanticsNode) {
       config.explicitChildNodes = true;
       config.isSemanticBoundary = true;
+    } else if (needsChildConfigrationsDelegate) {
+      config.childConfigurationsDelegate = _childSemanticsConfigurationsDelegate;
     } else {
-      if (_cachedAttributedLabel == null) {
+      if (_cachedAttributedLabels == null) {
         final StringBuffer buffer = StringBuffer();
         int offset = 0;
         final List<StringAttribute> attributes = <StringAttribute>[];
@@ -1050,21 +1063,77 @@
             final TextRange originalRange = infoAttribute.range;
             attributes.add(
               infoAttribute.copy(
-                  range: TextRange(start: offset + originalRange.start,
-                      end: offset + originalRange.end)
+                range: TextRange(
+                  start: offset + originalRange.start,
+                  end: offset + originalRange.end,
+                ),
               ),
             );
           }
           buffer.write(label);
           offset += label.length;
         }
-        _cachedAttributedLabel = AttributedString(buffer.toString(), attributes: attributes);
+        _cachedAttributedLabels = <AttributedString>[AttributedString(buffer.toString(), attributes: attributes)];
       }
-      config.attributedLabel = _cachedAttributedLabel!;
+      config.attributedLabel = _cachedAttributedLabels![0];
       config.textDirection = textDirection;
     }
   }
 
+  ChildSemanticsConfigurationsResult _childSemanticsConfigurationsDelegate(List<SemanticsConfiguration> childConfigs) {
+    final ChildSemanticsConfigurationsResultBuilder builder = ChildSemanticsConfigurationsResultBuilder();
+    int placeholderIndex = 0;
+    int childConfigsIndex = 0;
+    int attributedLabelCacheIndex = 0;
+    InlineSpanSemanticsInformation? seenTextInfo;
+    _cachedCombinedSemanticsInfos ??= combineSemanticsInfo(_semanticsInfo!);
+    for (final InlineSpanSemanticsInformation info in _cachedCombinedSemanticsInfos!) {
+      if (info.isPlaceholder) {
+        if (seenTextInfo != null) {
+          builder.markAsMergeUp(_createSemanticsConfigForTextInfo(seenTextInfo, attributedLabelCacheIndex));
+          attributedLabelCacheIndex += 1;
+        }
+        // Mark every childConfig belongs to this placeholder to merge up group.
+        while (childConfigsIndex < childConfigs.length &&
+            childConfigs[childConfigsIndex].tagsChildrenWith(PlaceholderSpanIndexSemanticsTag(placeholderIndex))) {
+          builder.markAsMergeUp(childConfigs[childConfigsIndex]);
+          childConfigsIndex += 1;
+        }
+        placeholderIndex += 1;
+      } else {
+        seenTextInfo = info;
+      }
+    }
+
+    // Handle plain text info at the end.
+    if (seenTextInfo != null) {
+      builder.markAsMergeUp(_createSemanticsConfigForTextInfo(seenTextInfo, attributedLabelCacheIndex));
+    }
+    return builder.build();
+  }
+
+  SemanticsConfiguration _createSemanticsConfigForTextInfo(InlineSpanSemanticsInformation textInfo, int cacheIndex) {
+    assert(!textInfo.requiresOwnNode);
+    final List<AttributedString> cachedStrings = _cachedAttributedLabels ??= <AttributedString>[];
+    assert(cacheIndex <= cachedStrings.length);
+    final bool hasCache = cacheIndex < cachedStrings.length;
+
+    late AttributedString attributedLabel;
+    if (hasCache) {
+      attributedLabel = cachedStrings[cacheIndex];
+    } else {
+      assert(cachedStrings.length == cacheIndex);
+      attributedLabel = AttributedString(
+        textInfo.semanticsLabel ?? textInfo.text,
+        attributes: textInfo.stringAttributes,
+      );
+      cachedStrings.add(attributedLabel);
+    }
+    return SemanticsConfiguration()
+      ..textDirection = textDirection
+      ..attributedLabel = attributedLabel;
+  }
+
   // Caches [SemanticsNode]s created during [assembleSemanticsNode] so they
   // can be re-used when [assembleSemanticsNode] is called again. This ensures
   // stable ids for the [SemanticsNode]s of [TextSpan]s across
diff --git a/framework/lib/src/rendering/stack.dart b/framework/lib/src/rendering/stack.dart
index d490d6b..92da839 100644
--- a/framework/lib/src/rendering/stack.dart
+++ b/framework/lib/src/rendering/stack.dart
@@ -165,8 +165,8 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static RelativeRect? lerp(RelativeRect? a, RelativeRect? b, double t) {
-    if (a == null && b == null) {
-      return null;
+    if (identical(a, b)) {
+      return a;
     }
     if (a == null) {
       return RelativeRect.fromLTRB(b!.left * t, b.top * t, b.right * t, b.bottom * t);
diff --git a/framework/lib/src/rendering/table_border.dart b/framework/lib/src/rendering/table_border.dart
index c4fecb7..bf7f5a3 100644
--- a/framework/lib/src/rendering/table_border.dart
+++ b/framework/lib/src/rendering/table_border.dart
@@ -153,8 +153,8 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static TableBorder? lerp(TableBorder? a, TableBorder? b, double t) {
-    if (a == null && b == null) {
-      return null;
+    if (identical(a, b)) {
+      return a;
     }
     if (a == null) {
       return b!.scale(t);
diff --git a/framework/lib/src/scheduler/binding.dart b/framework/lib/src/scheduler/binding.dart
index 8af3176..0adcf7c 100644
--- a/framework/lib/src/scheduler/binding.dart
+++ b/framework/lib/src/scheduler/binding.dart
@@ -189,7 +189,7 @@
 /// See also:
 ///
 /// * [PerformanceModeRequestHandle] for more information on the lifecycle of the handle.
-typedef _PerformanceModeCleaupCallback = VoidCallback;
+typedef _PerformanceModeCleanupCallback = VoidCallback;
 
 /// An opaque handle that keeps a request for [DartPerformanceMode] active until
 /// disposed.
@@ -197,9 +197,9 @@
 /// To create a [PerformanceModeRequestHandle], use [SchedulerBinding.requestPerformanceMode].
 /// The component that makes the request is responsible for disposing the handle.
 class PerformanceModeRequestHandle {
-  PerformanceModeRequestHandle._(_PerformanceModeCleaupCallback this._cleanup);
+  PerformanceModeRequestHandle._(_PerformanceModeCleanupCallback this._cleanup);
 
-  _PerformanceModeCleaupCallback? _cleanup;
+  _PerformanceModeCleanupCallback? _cleanup;
 
   /// Call this method to signal to [SchedulerBinding] that a request for a [DartPerformanceMode]
   /// is no longer needed.
diff --git a/framework/lib/src/semantics/binding.dart b/framework/lib/src/semantics/binding.dart
index 73d563d..70f784b 100644
--- a/framework/lib/src/semantics/binding.dart
+++ b/framework/lib/src/semantics/binding.dart
@@ -2,22 +2,27 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import 'package:engine/ui.dart' as ui show AccessibilityFeatures, SemanticsUpdateBuilder;
+import 'package:engine/ui.dart' as ui show AccessibilityFeatures, SemanticsAction, SemanticsUpdateBuilder;
 
 import 'package:flute/foundation.dart';
+import 'package:flute/services.dart';
 
 import 'debug.dart';
 
 export 'package:engine/ui.dart' show AccessibilityFeatures, SemanticsUpdateBuilder;
 
 /// The glue between the semantics layer and the Flutter engine.
-// TODO(zanderso): move the remaining semantic related bindings here.
 mixin SemanticsBinding on BindingBase {
   @override
   void initInstances() {
     super.initInstances();
     _instance = this;
     _accessibilityFeatures = platformDispatcher.accessibilityFeatures;
+    platformDispatcher
+      ..onSemanticsEnabledChanged = _handleSemanticsEnabledChanged
+      ..onSemanticsAction = _handleSemanticsAction
+      ..onAccessibilityFeaturesChanged = handleAccessibilityFeaturesChanged;
+    _handleSemanticsEnabledChanged();
   }
 
   /// The current [SemanticsBinding], if one has been created.
@@ -28,10 +33,123 @@
   static SemanticsBinding get instance => BindingBase.checkInstance(_instance);
   static SemanticsBinding? _instance;
 
+  /// Whether semantics information must be collected.
+  ///
+  /// Returns true if either the platform has requested semantics information
+  /// to be generated or if [ensureSemantics] has been called otherwise.
+  ///
+  /// To get notified when this value changes register a listener with
+  /// [addSemanticsEnabledListener].
+  bool get semanticsEnabled {
+    assert(_semanticsEnabled.value == (_outstandingHandles > 0));
+    return _semanticsEnabled.value;
+  }
+  late final ValueNotifier<bool> _semanticsEnabled = ValueNotifier<bool>(platformDispatcher.semanticsEnabled);
+
+  /// Adds a `listener` to be called when [semanticsEnabled] changes.
+  ///
+  /// See also:
+  ///
+  ///  * [removeSemanticsEnabledListener] to remove the listener again.
+  ///  * [ValueNotifier.addListener], which documents how and when listeners are
+  ///    called.
+  void addSemanticsEnabledListener(VoidCallback listener) {
+    _semanticsEnabled.addListener(listener);
+  }
+
+  /// Removes a `listener` added by [addSemanticsEnabledListener].
+  ///
+  /// See also:
+  ///
+  ///  * [ValueNotifier.removeListener], which documents how listeners are
+  ///    removed.
+  void removeSemanticsEnabledListener(VoidCallback listener) {
+    _semanticsEnabled.removeListener(listener);
+  }
+
+  /// The number of clients registered to listen for semantics.
+  ///
+  /// The number is increased whenever [ensureSemantics] is called and decreased
+  /// when [SemanticsHandle.dispose] is called.
+  int get debugOutstandingSemanticsHandles => _outstandingHandles;
+  int _outstandingHandles = 0;
+
+  /// Creates a new [SemanticsHandle] and requests the collection of semantics
+  /// information.
+  ///
+  /// Semantics information are only collected when there are clients interested
+  /// in them. These clients express their interest by holding a
+  /// [SemanticsHandle].
+  ///
+  /// Clients can close their [SemanticsHandle] by calling
+  /// [SemanticsHandle.dispose]. Once all outstanding [SemanticsHandle] objects
+  /// are closed, semantics information are no longer collected.
+  SemanticsHandle ensureSemantics() {
+    assert(_outstandingHandles >= 0);
+    _outstandingHandles++;
+    assert(_outstandingHandles > 0);
+    _semanticsEnabled.value = true;
+    return SemanticsHandle._(_didDisposeSemanticsHandle);
+  }
+
+  void _didDisposeSemanticsHandle() {
+    assert(_outstandingHandles > 0);
+    _outstandingHandles--;
+    assert(_outstandingHandles >= 0);
+    _semanticsEnabled.value = _outstandingHandles > 0;
+  }
+
+  // Handle for semantics request from the platform.
+  SemanticsHandle? _semanticsHandle;
+
+  void _handleSemanticsEnabledChanged() {
+    if (platformDispatcher.semanticsEnabled) {
+      _semanticsHandle ??= ensureSemantics();
+    } else {
+      _semanticsHandle?.dispose();
+      _semanticsHandle = null;
+    }
+  }
+
+  void _handleSemanticsAction(int id, ui.SemanticsAction action, ByteData? args) {
+    performSemanticsAction(SemanticsActionEvent(
+      nodeId: id,
+      type: action,
+      arguments: args != null ? const StandardMessageCodec().decodeMessage(args) : null,
+    ));
+  }
+
+  /// Called whenever the platform requests an action to be performed on a
+  /// [SemanticsNode].
+  ///
+  /// This callback is invoked when a user interacts with the app via an
+  /// accessibility service (e.g. TalkBack and VoiceOver) and initiates an
+  /// action on the focused node.
+  ///
+  /// Bindings that mixin the [SemanticsBinding] must implement this method and
+  /// perform the given `action` on the [SemanticsNode] specified by
+  /// [SemanticsActionEvent.nodeId].
+  ///
+  /// See [dart:ui.PlatformDispatcher.onSemanticsAction].
+  @protected
+  void performSemanticsAction(SemanticsActionEvent action);
+
+  /// The currently active set of [AccessibilityFeatures].
+  ///
+  /// This is set when the binding is first initialized and updated whenever a
+  /// flag is changed.
+  ///
+  /// To listen to changes to accessibility features, create a
+  /// [WidgetsBindingObserver] and listen to
+  /// [WidgetsBindingObserver.didChangeAccessibilityFeatures].
+  ui.AccessibilityFeatures get accessibilityFeatures => _accessibilityFeatures;
+  late ui.AccessibilityFeatures _accessibilityFeatures;
+
   /// Called when the platform accessibility features change.
   ///
   /// See [dart:ui.PlatformDispatcher.onAccessibilityFeaturesChanged].
   @protected
+  @mustCallSuper
   void handleAccessibilityFeaturesChanged() {
     _accessibilityFeatures = platformDispatcher.accessibilityFeatures;
   }
@@ -46,17 +164,6 @@
     return ui.SemanticsUpdateBuilder();
   }
 
-  /// The currently active set of [AccessibilityFeatures].
-  ///
-  /// This is initialized the first time [runApp] is called and updated whenever
-  /// a flag is changed.
-  ///
-  /// To listen to changes to accessibility features, create a
-  /// [WidgetsBindingObserver] and listen to
-  /// [WidgetsBindingObserver.didChangeAccessibilityFeatures].
-  ui.AccessibilityFeatures get accessibilityFeatures => _accessibilityFeatures;
-  late ui.AccessibilityFeatures _accessibilityFeatures;
-
   /// The platform is requesting that animations be disabled or simplified.
   ///
   /// This setting can be overridden for testing or debugging by setting
@@ -72,3 +179,49 @@
     return value;
   }
 }
+
+/// An event to request a [SemanticsAction] of [type] to be performed on the
+/// [SemanticsNode] identified by [nodeId].
+///
+/// Used by [SemanticsBinding.performSemanticsAction].
+@immutable
+class SemanticsActionEvent {
+  /// Creates a [SemanticsActionEvent].
+  ///
+  /// The [type] and [nodeId] are required.
+  const SemanticsActionEvent({required this.type, required this.nodeId, this.arguments});
+
+  /// The type of action to be performed.
+  final ui.SemanticsAction type;
+
+  /// The id of the [SemanticsNode] on which the action is to be performed.
+  final int nodeId;
+
+  /// Optional arguments for the action.
+  final Object? arguments;
+}
+
+/// A reference to the semantics information generated by the framework.
+///
+/// Semantics information are only collected when there are clients interested
+/// in them. These clients express their interest by holding a
+/// [SemanticsHandle]. When the client no longer needs the
+/// semantics information, it must call [dispose] on the [SemanticsHandle] to
+/// close it. When all open [SemanticsHandle]s are disposed, the framework will
+/// stop updating the semantics information.
+///
+/// To obtain a [SemanticsHandle], call [SemanticsBinding.ensureSemantics].
+class SemanticsHandle {
+  SemanticsHandle._(this._onDispose);
+
+  final VoidCallback _onDispose;
+
+  /// Closes the semantics handle.
+  ///
+  /// When all the outstanding [SemanticsHandle] objects are closed, the
+  /// framework will stop generating semantics information.
+  @mustCallSuper
+  void dispose() {
+    _onDispose();
+  }
+}
diff --git a/framework/lib/src/semantics/semantics.dart b/framework/lib/src/semantics/semantics.dart
index 32fb160..d88b9a1 100644
--- a/framework/lib/src/semantics/semantics.dart
+++ b/framework/lib/src/semantics/semantics.dart
@@ -346,7 +346,7 @@
   }
 
   @override
-  int get hashCode => Object.hash(string, attributes,);
+  int get hashCode => Object.hash(string, attributes);
 
   @override
   String toString() {
@@ -3129,9 +3129,9 @@
 /// Owns [SemanticsNode] objects and notifies listeners of changes to the
 /// render tree semantics.
 ///
-/// To listen for semantic updates, call [PipelineOwner.ensureSemantics] to
-/// obtain a [SemanticsHandle]. This will create a [SemanticsOwner] if
-/// necessary.
+/// To listen for semantic updates, call [SemanticsBinding.ensureSemantics] or
+/// [PipelineOwner.ensureSemantics] to obtain a [SemanticsHandle]. This will
+/// create a [SemanticsOwner] if necessary.
 class SemanticsOwner extends ChangeNotifier {
   /// Creates a [SemanticsOwner] that manages zero or more [SemanticsNode] objects.
   SemanticsOwner({
@@ -3248,7 +3248,7 @@
     }
 
     // Default actions if no [handler] was provided.
-    if (action == SemanticsAction.showOnScreen && _nodes[id]!._showOnScreen != null) {
+    if (action == SemanticsAction.showOnScreen && _nodes[id]?._showOnScreen != null) {
       _nodes[id]!._showOnScreen!();
     }
   }
@@ -3805,7 +3805,8 @@
   /// which of them should be merged upwards into the parent SemanticsNode.
   ///
   /// The input list of [SemanticsConfiguration]s can be empty if the rendering
-  /// object of this semantics configuration is a leaf node.
+  /// object of this semantics configuration is a leaf node or child rendering
+  /// objects do not contribute to the semantics.
   ChildSemanticsConfigurationsDelegate? get childConfigurationsDelegate => _childConfigurationsDelegate;
   ChildSemanticsConfigurationsDelegate? _childConfigurationsDelegate;
   set childConfigurationsDelegate(ChildSemanticsConfigurationsDelegate? value) {
diff --git a/framework/lib/src/services/asset_manifest.dart b/framework/lib/src/services/asset_manifest.dart
index 879a17c..4f76ae9 100644
--- a/framework/lib/src/services/asset_manifest.dart
+++ b/framework/lib/src/services/asset_manifest.dart
@@ -30,14 +30,12 @@
   /// information.
   List<String> listAssets();
 
-  /// Retrieves metadata about an asset and its variants.
+  /// Retrieves metadata about an asset and its variants. Returns null if the
+  /// key was not found in the asset manifest.
   ///
-  /// Note that this method considers a main asset to be a variant of itself and
+  /// This method considers a main asset to be a variant of itself and
   /// includes it in the returned list.
-  ///
-  /// Throws an [ArgumentError] if [key] cannot be found within the manifest. To
-  /// avoid this, use a key obtained from the [listAssets] method.
-  List<AssetMetadata> getAssetVariants(String key);
+  List<AssetMetadata>? getAssetVariants(String key);
 }
 
 // Lazily parses the binary asset manifest into a data structure that's easier to work
@@ -64,14 +62,14 @@
   final Map<String, List<AssetMetadata>> _typeCastedData = <String, List<AssetMetadata>>{};
 
   @override
-  List<AssetMetadata> getAssetVariants(String key) {
+  List<AssetMetadata>? getAssetVariants(String key) {
     // We lazily delay typecasting to prevent a performance hiccup when parsing
     // large asset manifests. This is important to keep an app's first asset
     // load fast.
     if (!_typeCastedData.containsKey(key)) {
       final Object? variantData = _data[key];
       if (variantData == null) {
-        throw ArgumentError('Asset key $key was not found within the asset manifest.');
+        return null;
       }
       _typeCastedData[key] = ((_data[key] ?? <Object?>[]) as Iterable<Object?>)
         .cast<Map<Object?, Object?>>()
diff --git a/framework/lib/src/services/dom.dart b/framework/lib/src/services/dom.dart
index fad6f16..96a44a4 100644
--- a/framework/lib/src/services/dom.dart
+++ b/framework/lib/src/services/dom.dart
@@ -115,7 +115,7 @@
 @staticInterop
 class DomEvent {}
 
-/// [DomEvent] reqiured extension.
+/// [DomEvent] required extension.
 extension DomEventExtension on DomEvent {
   /// Get the event type.
   external String get type;
@@ -134,7 +134,7 @@
 @staticInterop
 class DomProgressEvent extends DomEvent {}
 
-/// [DomProgressEvent] reqiured extension.
+/// [DomProgressEvent] required extension.
 extension DomProgressEventExtension on DomProgressEvent {
   /// Amount of work done.
   external int? get loaded;
@@ -182,7 +182,7 @@
 @JS('window.document')
 external DomDocument get domDocument;
 
-/// Cretaes a new DOM event.
+/// Creates a new DOM event.
 DomEvent createDomEvent(String type, String name) {
   final DomEvent event = domDocument.createEvent(type);
   event.initEvent(name, true, true);
@@ -336,8 +336,11 @@
 /// [DomCSSStyleSheet]'s required extension.
 extension DomCSSStyleSheetExtension on DomCSSStyleSheet {
   /// Inserts a rule into this style sheet.
-  int insertRule(String rule, [int? index]) => js_util
-      .callMethod(this, 'insertRule', <Object>[rule, if (index != null) index]);
+  int insertRule(String rule, [int? index]) =>
+    js_util.callMethod<double>(this, 'insertRule', <Object>[
+      rule,
+      if (index != null) index.toDouble()
+    ]).toInt();
 }
 
 /// A list of token.
diff --git a/framework/lib/src/services/hardware_keyboard.dart b/framework/lib/src/services/hardware_keyboard.dart
index 7fcbe1a..67cc896 100644
--- a/framework/lib/src/services/hardware_keyboard.dart
+++ b/framework/lib/src/services/hardware_keyboard.dart
@@ -864,7 +864,7 @@
         return false;
       case KeyDataTransitMode.keyDataThenRawKeyData:
         // Having 0 as the physical and logical ID indicates an empty key data
-        // (the only occassion either field can be 0,) transmitted to ensure
+        // (the only occasion either field can be 0,) transmitted to ensure
         // that the transit mode is correctly inferred. These events should be
         // ignored.
         if (data.physical == 0 && data.logical == 0) {
diff --git a/framework/lib/src/services/platform_views.dart b/framework/lib/src/services/platform_views.dart
index 7825904..3c2e338 100644
--- a/framework/lib/src/services/platform_views.dart
+++ b/framework/lib/src/services/platform_views.dart
@@ -44,7 +44,16 @@
   ///
   /// Typically a platform view identifier is passed to a platform view widget
   /// which creates the platform view and manages its lifecycle.
-  int getNextPlatformViewId() => _nextPlatformViewId++;
+  int getNextPlatformViewId() {
+    // On the Android side, the interface exposed to users uses 32-bit integers.
+    // See https://github.com/flutter/engine/pull/39476 for more details.
+
+    // We can safely assume that a Flutter application will not require more
+    // than MAX_INT32 platform views during its lifetime.
+    const int MAX_INT32 = 0x7FFFFFFF;
+    assert(_nextPlatformViewId <= MAX_INT32);
+    return _nextPlatformViewId++;
+  }
 }
 
 /// Callback signature for when a platform view was created.
diff --git a/framework/lib/src/services/raw_keyboard.dart b/framework/lib/src/services/raw_keyboard.dart
index 842af59..4cd8314 100644
--- a/framework/lib/src/services/raw_keyboard.dart
+++ b/framework/lib/src/services/raw_keyboard.dart
@@ -432,10 +432,10 @@
   /// Returns true if a ALT modifier key is pressed, regardless of which side
   /// of the keyboard it is on.
   ///
-  /// Note that the ALTGR key that appears on some keyboards is considered to be
+  /// The `AltGr` key that appears on some keyboards is considered to be
   /// the same as [LogicalKeyboardKey.altRight] on some platforms (notably
   /// Android). On platforms that can distinguish between `altRight` and
-  /// `altGr`, a press of `altGr` will not return true here, and will need to be
+  /// `altGr`, a press of `AltGr` will not return true here, and will need to be
   /// tested for separately.
   ///
   /// Use [isKeyPressed] if you need to know which alt key was pressed.
@@ -864,7 +864,7 @@
     // exist in the modifier list. Enforce the pressing state.
     if (event is RawKeyDownEvent && thisKeyModifier != null
         && !_keysPressed.containsKey(event.physicalKey)) {
-      // This inconsistancy is found on Linux GTK for AltRight:
+      // This inconsistency is found on Linux GTK for AltRight:
       // https://github.com/flutter/flutter/issues/93278
       // And also on Android and iOS:
       // https://github.com/flutter/flutter/issues/101090
diff --git a/framework/lib/src/services/system_chrome.dart b/framework/lib/src/services/system_chrome.dart
index 6b0ed70..ad42357 100644
--- a/framework/lib/src/services/system_chrome.dart
+++ b/framework/lib/src/services/system_chrome.dart
@@ -531,9 +531,9 @@
   /// to configure the system styles when an app bar is not used. When an app
   /// bar is used, apps should not enclose the app bar in an annotated region
   /// because one is automatically created. If an app bar is used and the app
-  /// bar is enclosed in an annotated region, the app bar overlay style supercedes
+  /// bar is enclosed in an annotated region, the app bar overlay style supersedes
   /// the status bar properties defined in the enclosing annotated region overlay
-  /// style and the enclosing annotated region overlay style supercedes the app bar
+  /// style and the enclosing annotated region overlay style supersedes the app bar
   /// overlay style navigation bar properties.
   ///
   /// {@tool sample}
diff --git a/framework/lib/src/services/text_boundary.dart b/framework/lib/src/services/text_boundary.dart
index 4e2414e..ff6f318 100644
--- a/framework/lib/src/services/text_boundary.dart
+++ b/framework/lib/src/services/text_boundary.dart
@@ -59,7 +59,7 @@
   }
 }
 
-/// A [TextBoundary] subclass for retriving the range of the grapheme the given
+/// A [TextBoundary] subclass for retrieving the range of the grapheme the given
 /// `position` is in.
 ///
 /// The class is implemented using the
diff --git a/framework/lib/src/services/text_editing_delta.dart b/framework/lib/src/services/text_editing_delta.dart
index 91872c6..8ca66b6 100644
--- a/framework/lib/src/services/text_editing_delta.dart
+++ b/framework/lib/src/services/text_editing_delta.dart
@@ -277,7 +277,7 @@
   @override
   TextEditingValue apply(TextEditingValue value) {
     // To stay inline with the plain text model we should follow a last write wins
-    // policy and apply the delta to the oldText. This is due to the asyncronous
+    // policy and apply the delta to the oldText. This is due to the asynchronous
     // nature of the connection between the framework and platform text input plugins.
     String newText = oldText;
     assert(_debugTextRangeIsValid(TextRange.collapsed(insertionOffset), newText), 'Applying TextEditingDeltaInsertion failed, the insertionOffset: $insertionOffset is not within the bounds of $newText of length: ${newText.length}');
@@ -323,7 +323,7 @@
   @override
   TextEditingValue apply(TextEditingValue value) {
     // To stay inline with the plain text model we should follow a last write wins
-    // policy and apply the delta to the oldText. This is due to the asyncronous
+    // policy and apply the delta to the oldText. This is due to the asynchronous
     // nature of the connection between the framework and platform text input plugins.
     String newText = oldText;
     assert(_debugTextRangeIsValid(deletedRange, newText), 'Applying TextEditingDeltaDeletion failed, the deletedRange: $deletedRange is not within the bounds of $newText of length: ${newText.length}');
@@ -379,7 +379,7 @@
   @override
   TextEditingValue apply(TextEditingValue value) {
     // To stay inline with the plain text model we should follow a last write wins
-    // policy and apply the delta to the oldText. This is due to the asyncronous
+    // policy and apply the delta to the oldText. This is due to the asynchronous
     // nature of the connection between the framework and platform text input plugins.
     String newText = oldText;
     assert(_debugTextRangeIsValid(replacedRange, newText), 'Applying TextEditingDeltaReplacement failed, the replacedRange: $replacedRange is not within the bounds of $newText of length: ${newText.length}');
@@ -425,7 +425,7 @@
   @override
   TextEditingValue apply(TextEditingValue value) {
     // To stay inline with the plain text model we should follow a last write wins
-    // policy and apply the delta to the oldText. This is due to the asyncronous
+    // policy and apply the delta to the oldText. This is due to the asynchronous
     // nature of the connection between the framework and platform text input plugins.
     assert(_debugTextRangeIsValid(selection, oldText), 'Applying TextEditingDeltaNonTextUpdate failed, the selection range: $selection is not within the bounds of $oldText of length: ${oldText.length}');
     assert(_debugTextRangeIsValid(composing, oldText), 'Applying TextEditingDeltaNonTextUpdate failed, the composing region: $composing is not within the bounds of $oldText of length: ${oldText.length}');
diff --git a/framework/lib/src/widgets/app.dart b/framework/lib/src/widgets/app.dart
index a5f6449..09880c2 100644
--- a/framework/lib/src/widgets/app.dart
+++ b/framework/lib/src/widgets/app.dart
@@ -252,8 +252,6 @@
 ///    without an explicit style.
 ///  * [MediaQuery], which establishes a subtree in which media queries resolve
 ///    to a [MediaQueryData].
-///  * [MediaQuery.fromWindow], which builds a [MediaQuery] with data derived
-///    from [WidgetsBinding.window].
 ///  * [Localizations], which defines the [Locale] for its `child`.
 ///  * [Title], a widget that describes this app in the operating system.
 ///  * [Navigator], a widget that manages a set of child widgets with a stack
diff --git a/framework/lib/src/widgets/basic.dart b/framework/lib/src/widgets/basic.dart
index 29c0181..d282ff6 100644
--- a/framework/lib/src/widgets/basic.dart
+++ b/framework/lib/src/widgets/basic.dart
@@ -6811,7 +6811,7 @@
 /// A widget that annotates the widget tree with a description of the meaning of
 /// the widgets.
 ///
-/// Used by assitive technologies, search engines, and other semantic analysis
+/// Used by assistive technologies, search engines, and other semantic analysis
 /// software to determine the meaning of the application.
 ///
 /// {@youtube 560 315 https://www.youtube.com/watch?v=NvtMt_DtFrQ}
diff --git a/framework/lib/src/widgets/binding.dart b/framework/lib/src/widgets/binding.dart
index 7633f50..f1e3846 100644
--- a/framework/lib/src/widgets/binding.dart
+++ b/framework/lib/src/widgets/binding.dart
@@ -260,7 +260,6 @@
     _buildOwner = BuildOwner();
     buildOwner!.onBuildScheduled = _handleBuildScheduled;
     platformDispatcher.onLocaleChanged = handleLocaleChanged;
-    platformDispatcher.onAccessibilityFeaturesChanged = handleAccessibilityFeaturesChanged;
     SystemChannels.navigation.setMethodCallHandler(_handleNavigationInvocation);
     assert(() {
       FlutterErrorDetails.propertiesTransformers.add(debugTransformDebugCreator);
@@ -902,15 +901,13 @@
   /// Used by [runApp] to wrap the provided `rootWidget` in the default [View].
   ///
   /// The [View] determines into what [FlutterView] the app is rendered into.
-  /// For backwards-compatibility reasons, this method currently chooses
-  /// [window] (which is a [FlutterView]) as the rendering target. This will
-  /// change in a future version of Flutter.
+  /// This is currently [PlatformDispatcher.implicitView] from [platformDispatcher].
   ///
   /// The `rootWidget` widget provided to this method must not already be
   /// wrapped in a [View].
   Widget wrapWithDefaultView(Widget rootWidget) {
     return View(
-      view: window,
+      view: platformDispatcher.implicitView!,
       child: rootWidget,
     );
   }
diff --git a/framework/lib/src/widgets/default_text_editing_shortcuts.dart b/framework/lib/src/widgets/default_text_editing_shortcuts.dart
index edee66f..19e9d97 100644
--- a/framework/lib/src/widgets/default_text_editing_shortcuts.dart
+++ b/framework/lib/src/widgets/default_text_editing_shortcuts.dart
@@ -34,7 +34,7 @@
 /// ```dart
 /// @override
 /// Widget build(BuildContext context) {
-///   // If using WidgetsApp or its descendents MaterialApp or CupertinoApp,
+///   // If using WidgetsApp or its descendants MaterialApp or CupertinoApp,
 ///   // then DefaultTextEditingShortcuts is already being inserted into the
 ///   // widget tree.
 ///   return const DefaultTextEditingShortcuts(
@@ -89,7 +89,7 @@
 ///
 ///   @override
 ///   Widget build(BuildContext context) {
-///     // If using WidgetsApp or its descendents MaterialApp or CupertinoApp,
+///     // If using WidgetsApp or its descendants MaterialApp or CupertinoApp,
 ///     // then DefaultTextEditingShortcuts is already being inserted into the
 ///     // widget tree.
 ///     return DefaultTextEditingShortcuts(
diff --git a/framework/lib/src/widgets/drag_target.dart b/framework/lib/src/widgets/drag_target.dart
index 950d30a..1d23dae 100644
--- a/framework/lib/src/widgets/drag_target.dart
+++ b/framework/lib/src/widgets/drag_target.dart
@@ -648,7 +648,7 @@
 
   /// Called when a [Draggable] moves within this [DragTarget].
   ///
-  /// Note that this includes entering and leaving the target.
+  /// This includes entering and leaving the target.
   final DragTargetMove<T>? onMove;
 
   /// How to behave during hit testing.
diff --git a/framework/lib/src/widgets/draggable_scrollable_sheet.dart b/framework/lib/src/widgets/draggable_scrollable_sheet.dart
index 090fa0e..dc27024 100644
--- a/framework/lib/src/widgets/draggable_scrollable_sheet.dart
+++ b/framework/lib/src/widgets/draggable_scrollable_sheet.dart
@@ -531,8 +531,8 @@
   /// Start an activity that affects the sheet and register a cancel call back
   /// that will be called if another activity starts.
   ///
-  /// Note that `onCanceled` will get called even if the subsequent activity
-  /// started after this one finished so `onCanceled` should be safe to call at
+  /// The `onCanceled` callback will get called even if the subsequent activity
+  /// started after this one finished, so `onCanceled` must be safe to call at
   /// any time.
   void startActivity({required VoidCallback onCanceled}) {
     _cancelActivity?.call();
diff --git a/framework/lib/src/widgets/editable_text.dart b/framework/lib/src/widgets/editable_text.dart
index 02093ae..a239d99 100644
--- a/framework/lib/src/widgets/editable_text.dart
+++ b/framework/lib/src/widgets/editable_text.dart
@@ -2368,7 +2368,7 @@
   SuggestionSpan? findSuggestionSpanAtCursorIndex(int cursorIndex) {
     if (!_spellCheckResultsReceived
         || spellCheckResults!.suggestionSpans.last.range.end < cursorIndex) {
-      // No spell check results have been recieved or the cursor index is out
+      // No spell check results have been received or the cursor index is out
       // of range that suggestionSpans covers.
       return null;
     }
@@ -2533,7 +2533,7 @@
   ///
   /// * [EditableText.getEditableButtonItems], which performs a similar role,
   ///   but for any editable field, not just specifically EditableText.
-  /// * [SelectableRegionState.contextMenuButtonItems], which peforms a similar
+  /// * [SelectableRegionState.contextMenuButtonItems], which performs a similar
   ///   role but for content that is selectable but not editable.
   /// * [contextMenuAnchors], which provides the anchor points for the default
   ///   context menu.
@@ -2605,7 +2605,7 @@
       if (_tickersEnabled && _cursorActive) {
         _startCursorBlink();
       } else if (!_tickersEnabled && _cursorTimer != null) {
-        // Cannot use _stopCursorTimer because it would reset _cursorActive.
+        // Cannot use _stopCursorBlink because it would reset _cursorActive.
         _cursorTimer!.cancel();
         _cursorTimer = null;
       }
@@ -2789,7 +2789,7 @@
     }
 
     // Wherever the value is changed by the user, schedule a showCaretOnScreen
-    // to make sure the user can see the changes they just made. Programmatical
+    // to make sure the user can see the changes they just made. Programmatic
     // changes to `textEditingValue` do not trigger the behavior even if the
     // text field is focused.
     _scheduleShowCaretOnScreen(withAnimation: true);
@@ -3557,6 +3557,8 @@
       }
     }
 
+    final TextSelection oldTextSelection = textEditingValue.selection;
+
     // Put all optional user callback invocations in a batch edit to prevent
     // sending multiple `TextInput.updateEditingValue` messages.
     beginBatchEdit();
@@ -3570,6 +3572,7 @@
         (cause == SelectionChangedCause.longPress ||
          cause == SelectionChangedCause.keyboard))) {
       _handleSelectionChanged(_value.selection, cause);
+      _bringIntoViewBySelectionState(oldTextSelection, value.selection, cause);
     }
     final String currentText = _value.text;
     if (oldValue.text != currentText) {
@@ -3587,6 +3590,30 @@
     endBatchEdit();
   }
 
+  void _bringIntoViewBySelectionState(TextSelection oldSelection, TextSelection newSelection, SelectionChangedCause? cause) {
+    switch (defaultTargetPlatform) {
+      case TargetPlatform.iOS:
+      case TargetPlatform.macOS:
+        if (cause == SelectionChangedCause.longPress ||
+            cause == SelectionChangedCause.drag) {
+          bringIntoView(newSelection.extent);
+        }
+        break;
+      case TargetPlatform.linux:
+      case TargetPlatform.windows:
+      case TargetPlatform.fuchsia:
+      case TargetPlatform.android:
+        if (cause == SelectionChangedCause.drag) {
+          if (oldSelection.baseOffset != newSelection.baseOffset) {
+            bringIntoView(newSelection.base);
+          } else if (oldSelection.extentOffset != newSelection.extentOffset) {
+            bringIntoView(newSelection.extent);
+          }
+        }
+        break;
+    }
+  }
+
   void _onCursorColorTick() {
     renderEditable.cursorColor = widget.cursorColor.withOpacity(_cursorBlinkOpacityController.value);
     _cursorVisibilityNotifier.value = widget.showCursor && _cursorBlinkOpacityController.value > 0;
@@ -4565,14 +4592,14 @@
       String text = _value.text;
       text = widget.obscuringCharacter * text.length;
       // Reveal the latest character in an obscured field only on mobile.
-      // Newer verions of iOS (iOS 15+) no longer reveal the most recently
+      // Newer versions of iOS (iOS 15+) no longer reveal the most recently
       // entered character.
       const Set<TargetPlatform> mobilePlatforms = <TargetPlatform> {
         TargetPlatform.android, TargetPlatform.fuchsia,
       };
-      final bool breiflyShowPassword = WidgetsBinding.instance.platformDispatcher.brieflyShowPassword
+      final bool brieflyShowPassword = WidgetsBinding.instance.platformDispatcher.brieflyShowPassword
                                     && mobilePlatforms.contains(defaultTargetPlatform);
-      if (breiflyShowPassword) {
+      if (brieflyShowPassword) {
         final int? o = _obscureShowCharTicksPending > 0 ? _obscureLatestCharIndex : null;
         if (o != null && o >= 0 && o < text.length) {
           text = text.replaceRange(o, o + 1, _value.text.substring(o, o + 1));
@@ -5285,6 +5312,10 @@
   late final _Throttled<TextEditingValue> _throttledPush;
   Timer? _throttleTimer;
 
+  // This is used to prevent a reentrant call to the history (a call to _undo or _redo
+  // should not call _push to add a new entry in the history).
+  bool _locked = false;
+
   // This duration was chosen as a best fit for the behavior of Mac, Linux,
   // and Windows undo/redo state save durations, but it is not perfect for any
   // of them.
@@ -5305,13 +5336,19 @@
     if (nextValue.text == widget.controller.text) {
       return;
     }
+    _locked = true;
     widget.onTriggered(widget.controller.value.copyWith(
       text: nextValue.text,
       selection: nextValue.selection,
     ));
+    _locked = false;
   }
 
   void _push() {
+    // Do not try to push a new state when the change is related to an undo or redo.
+    if (_locked) {
+      return;
+    }
     if (widget.controller.value == TextEditingValue.empty) {
       return;
     }
@@ -5385,8 +5422,8 @@
 
   final List<T> _list = <T>[];
 
-  // The index of the current value, or null if the list is emtpy.
-  late int _index;
+  // The index of the current value, or -1 if the list is empty.
+  int _index = -1;
 
   /// Returns the current value of the stack.
   T? get currentValue => _list.isEmpty ? null : _list[_index];
diff --git a/framework/lib/src/widgets/focus_manager.dart b/framework/lib/src/widgets/focus_manager.dart
index a0226ea..dc264ad 100644
--- a/framework/lib/src/widgets/focus_manager.dart
+++ b/framework/lib/src/widgets/focus_manager.dart
@@ -1082,7 +1082,7 @@
     _doRequestFocus(findFirstFocus: true);
   }
 
-  // Note that this is overridden in FocusScopeNode.
+  // This is overridden in FocusScopeNode.
   void _doRequestFocus({required bool findFirstFocus}) {
     if (!canRequestFocus) {
       assert(_focusDebug(() => 'Node NOT requesting focus because canRequestFocus is false: $this'));
diff --git a/framework/lib/src/widgets/focus_scope.dart b/framework/lib/src/widgets/focus_scope.dart
index 81bcb45..84668c5 100644
--- a/framework/lib/src/widgets/focus_scope.dart
+++ b/framework/lib/src/widgets/focus_scope.dart
@@ -319,7 +319,7 @@
   /// {@template flutter.widgets.Focus.descendantsAreTraversable}
   /// If false, will make this widget's descendants untraversable.
   ///
-  /// Defaults to true. Does not affect traversablility of this node (just its
+  /// Defaults to true. Does not affect traversability of this node (just its
   /// descendants): for that, use [FocusNode.skipTraversal].
   ///
   /// Does not affect the value of [FocusNode.skipTraversal] on the
@@ -370,21 +370,17 @@
   /// given [BuildContext].
   ///
   /// If no [Focus] node is found before reaching the nearest [FocusScope]
-  /// widget, or there is no [Focus] widget in scope, then this method will
-  /// throw an exception.
+  /// widget, or there is no [Focus] widget in the context, then this method
+  /// will throw an exception.
   ///
-  /// The `context` and `scopeOk` arguments must not be null.
-  ///
-  /// Calling this function creates a dependency that will rebuild the given
-  /// context when the focus changes.
+  /// {@macro flutter.widgets.focus_scope.Focus.maybeOf}
   ///
   /// See also:
   ///
-  ///  * [maybeOf], which is similar to this function, but will return null
-  ///    instead of throwing if it doesn't find a [Focus] node.
-  static FocusNode of(BuildContext context, { bool scopeOk = false }) {
-    final _FocusInheritedScope? marker = context.dependOnInheritedWidgetOfExactType<_FocusInheritedScope>();
-    final FocusNode? node = marker?.notifier;
+  /// * [maybeOf], which is similar to this function, but will return null
+  ///   instead of throwing if it doesn't find a [Focus] node.
+  static FocusNode of(BuildContext context, { bool scopeOk = false, bool createDependency = true }) {
+    final FocusNode? node = Focus.maybeOf(context, scopeOk: scopeOk, createDependency: createDependency);
     assert(() {
       if (node == null) {
         throw FlutterError(
@@ -423,18 +419,24 @@
   /// widget, or there is no [Focus] widget in scope, then this method will
   /// return null.
   ///
-  /// The `context` and `scopeOk` arguments must not be null.
-  ///
-  /// Calling this function creates a dependency that will rebuild the given
-  /// context when the focus changes.
+  /// {@template flutter.widgets.focus_scope.Focus.maybeOf}
+  /// If `createDependency` is true (which is the default), calling this
+  /// function creates a dependency that will rebuild the given context when the
+  /// focus node gains or loses focus.
+  /// {@endtemplate}
   ///
   /// See also:
   ///
-  ///  * [of], which is similar to this function, but will throw an exception if
-  ///    it doesn't find a [Focus] node instead of returning null.
-  static FocusNode? maybeOf(BuildContext context, { bool scopeOk = false }) {
-    final _FocusInheritedScope? marker = context.dependOnInheritedWidgetOfExactType<_FocusInheritedScope>();
-    final FocusNode? node = marker?.notifier;
+  /// * [of], which is similar to this function, but will throw an exception if
+  ///   it doesn't find a [Focus] node, instead of returning null.
+  static FocusNode? maybeOf(BuildContext context, { bool scopeOk = false, bool createDependency = true }) {
+    final _FocusInheritedScope? scope;
+    if (createDependency) {
+      scope = context.dependOnInheritedWidgetOfExactType<_FocusInheritedScope>();
+    } else {
+      scope = context.getInheritedWidgetOfExactType<_FocusInheritedScope>();
+    }
+    final FocusNode? node = scope?.notifier;
     if (node == null) {
       return null;
     }
@@ -776,16 +778,16 @@
     ValueChanged<bool>? onFocusChange,
   })  = _FocusScopeWithExternalFocusNode;
 
-  /// Returns the [FocusScopeNode] of the [FocusScope] that most tightly
-  /// encloses the given [context].
+  /// Returns the [FocusNode.nearestScope] of the [Focus] or [FocusScope] that
+  /// most tightly encloses the given [context].
   ///
-  /// If this node doesn't have a [Focus] widget ancestor, then the
-  /// [FocusManager.rootScope] is returned.
+  /// If this node doesn't have a [Focus] or [FocusScope] widget ancestor, then
+  /// the [FocusManager.rootScope] is returned.
   ///
-  /// The [context] argument must not be null.
-  static FocusScopeNode of(BuildContext context) {
-    final _FocusInheritedScope? marker = context.dependOnInheritedWidgetOfExactType<_FocusInheritedScope>();
-    return marker?.notifier?.nearestScope ?? context.owner!.focusManager.rootScope;
+  /// {@macro flutter.widgets.focus_scope.Focus.maybeOf}
+  static FocusScopeNode of(BuildContext context, { bool createDependency = true }) {
+    return Focus.maybeOf(context, scopeOk: true, createDependency: createDependency)?.nearestScope
+        ?? context.owner!.focusManager.rootScope;
   }
 
   @override
diff --git a/framework/lib/src/widgets/focus_traversal.dart b/framework/lib/src/widgets/focus_traversal.dart
index ec3a40d..c683bf0 100644
--- a/framework/lib/src/widgets/focus_traversal.dart
+++ b/framework/lib/src/widgets/focus_traversal.dart
@@ -47,11 +47,11 @@
 // sorting their contents.
 class _FocusTraversalGroupInfo {
   _FocusTraversalGroupInfo(
-    _FocusTraversalGroupScope? marker, {
+    _FocusTraversalGroupNode? group, {
     FocusTraversalPolicy? defaultPolicy,
     List<FocusNode>? members,
-  })  : groupNode = marker?.focusNode,
-        policy = marker?.policy ?? defaultPolicy ?? ReadingOrderTraversalPolicy(),
+  })  : groupNode = group,
+        policy = group?.policy ?? defaultPolicy ?? ReadingOrderTraversalPolicy(),
         members = members ?? <FocusNode>[];
 
   final FocusNode? groupNode;
@@ -114,7 +114,7 @@
   /// current [FlutterView]. For example, [NextFocusAction] invoked via keyboard
   /// (typically the TAB key) would receive [KeyEventResult.skipRemainingHandlers]
   /// allowing the embedder handle the shortcut. On the web, typically the
-  /// control is transfered to the browser, allowing the user to reach the
+  /// control is transferred to the browser, allowing the user to reach the
   /// address bar, escape an `iframe`, or focus on HTML elements other than
   /// those managed by Flutter.
   leaveFlutterView,
@@ -318,45 +318,43 @@
   @protected
   Iterable<FocusNode> sortDescendants(Iterable<FocusNode> descendants, FocusNode currentNode);
 
-  _FocusTraversalGroupScope? _getMarker(BuildContext? context) {
-    return context?.getElementForInheritedWidgetOfExactType<_FocusTraversalGroupScope>()?.widget as _FocusTraversalGroupScope?;
-  }
-
-  // Sort all descendants, taking into account the FocusTraversalGroup
-  // that they are each in, and filtering out non-traversable/focusable nodes.
-  List<FocusNode> _sortAllDescendants(FocusScopeNode scope, FocusNode currentNode) {
-    final _FocusTraversalGroupScope? scopeGroupMarker = _getMarker(scope.context);
-    final FocusTraversalPolicy defaultPolicy = scopeGroupMarker?.policy ?? ReadingOrderTraversalPolicy();
-    // Build the sorting data structure, separating descendants into groups.
+  Map<FocusNode?, _FocusTraversalGroupInfo> _findGroups(FocusScopeNode scope, _FocusTraversalGroupNode? scopeGroupNode) {
+    final FocusTraversalPolicy defaultPolicy = scopeGroupNode?.policy ?? ReadingOrderTraversalPolicy();
     final Map<FocusNode?, _FocusTraversalGroupInfo> groups = <FocusNode?, _FocusTraversalGroupInfo>{};
     for (final FocusNode node in scope.descendants) {
-      final _FocusTraversalGroupScope? groupMarker = _getMarker(node.context);
-      final FocusNode? groupNode = groupMarker?.focusNode;
+      final _FocusTraversalGroupNode? groupNode = FocusTraversalGroup._getGroupNode(node);
       // Group nodes need to be added to their parent's node, or to the "null"
       // node if no parent is found. This creates the hierarchy of group nodes
       // and makes it so the entire group is sorted along with the other members
       // of the parent group.
       if (node == groupNode) {
         // To find the parent of the group node, we need to skip over the parent
-        // of the Focus node in _FocusTraversalGroupState.build, and start
-        // looking with that node's parent, since _getMarker will return the
-        // context it was called on if it matches the type.
-        final BuildContext? parentContext = _getAncestor(groupNode!.context!, count: 2);
-        final _FocusTraversalGroupScope? parentMarker = _getMarker(parentContext);
-        final FocusNode? parentNode = parentMarker?.focusNode;
-        groups[parentNode] ??= _FocusTraversalGroupInfo(parentMarker, members: <FocusNode>[], defaultPolicy: defaultPolicy);
-        assert(!groups[parentNode]!.members.contains(node));
-        groups[parentNode]!.members.add(groupNode);
+        // of the Focus node added in _FocusTraversalGroupState.build, and start
+        // looking with that node's parent, since _getGroupNode will return the
+        // node it was called on if it matches the type.
+        final _FocusTraversalGroupNode? parentGroup = FocusTraversalGroup._getGroupNode(groupNode!.parent!);
+        groups[parentGroup] ??= _FocusTraversalGroupInfo(parentGroup, members: <FocusNode>[], defaultPolicy: defaultPolicy);
+        assert(!groups[parentGroup]!.members.contains(node));
+        groups[parentGroup]!.members.add(groupNode);
         continue;
       }
       // Skip non-focusable and non-traversable nodes in the same way that
       // FocusScopeNode.traversalDescendants would.
       if (node.canRequestFocus && !node.skipTraversal) {
-        groups[groupNode] ??= _FocusTraversalGroupInfo(groupMarker, members: <FocusNode>[], defaultPolicy: defaultPolicy);
+        groups[groupNode] ??= _FocusTraversalGroupInfo(groupNode, members: <FocusNode>[], defaultPolicy: defaultPolicy);
         assert(!groups[groupNode]!.members.contains(node));
         groups[groupNode]!.members.add(node);
       }
     }
+    return groups;
+  }
+
+  // Sort all descendants, taking into account the FocusTraversalGroup
+  // that they are each in, and filtering out non-traversable/focusable nodes.
+  List<FocusNode> _sortAllDescendants(FocusScopeNode scope, FocusNode currentNode) {
+    final _FocusTraversalGroupNode? scopeGroupNode = FocusTraversalGroup._getGroupNode(scope);
+    // Build the sorting data structure, separating descendants into groups.
+    final Map<FocusNode?, _FocusTraversalGroupInfo> groups = _findGroups(scope, scopeGroupNode);
 
     // Sort the member lists using the individual policy sorts.
     for (final FocusNode? key in groups.keys) {
@@ -381,8 +379,8 @@
     }
 
     // Visit the children of the scope, if any.
-    if (groups.isNotEmpty && groups.containsKey(scopeGroupMarker?.focusNode)) {
-      visitGroups(groups[scopeGroupMarker?.focusNode]!);
+    if (groups.isNotEmpty && groups.containsKey(scopeGroupNode)) {
+      visitGroups(groups[scopeGroupNode]!);
     }
 
     // Remove the FocusTraversalGroup nodes themselves, which aren't focusable.
@@ -941,7 +939,7 @@
   // Find the directionality in force for a build context without creating a
   // dependency.
   static TextDirection? _findDirectionality(BuildContext context) {
-    return (context.getElementForInheritedWidgetOfExactType<Directionality>()?.widget as Directionality?)?.textDirection;
+    return context.getInheritedWidgetOfExactType<Directionality>()?.textDirection;
   }
 
   /// Finds the common Directional ancestor of an entire list of groups.
@@ -1439,7 +1437,7 @@
   /// If no [FocusTraversalOrder] ancestor exists, or the order is null, this
   /// will assert in debug mode, and throw an exception in release mode.
   static FocusOrder of(BuildContext context) {
-    final FocusTraversalOrder? marker = context.getElementForInheritedWidgetOfExactType<FocusTraversalOrder>()?.widget as FocusTraversalOrder?;
+    final FocusTraversalOrder? marker = context.getInheritedWidgetOfExactType<FocusTraversalOrder>();
     assert(() {
       if (marker == null) {
         throw FlutterError(
@@ -1464,7 +1462,7 @@
   ///
   /// If no [FocusTraversalOrder] ancestor exists, or the order is null, returns null.
   static FocusOrder? maybeOf(BuildContext context) {
-    final FocusTraversalOrder? marker = context.getElementForInheritedWidgetOfExactType<FocusTraversalOrder>()?.widget as FocusTraversalOrder?;
+    final FocusTraversalOrder? marker = context.getInheritedWidgetOfExactType<FocusTraversalOrder>();
     return marker?.order;
   }
 
@@ -1556,56 +1554,107 @@
   /// {@macro flutter.widgets.ProxyWidget.child}
   final Widget child;
 
-  /// Returns the focus policy set by the [FocusTraversalGroup] that most
-  /// tightly encloses the given [BuildContext].
+  /// Returns the [FocusTraversalPolicy] that applies to the nearest ancestor of
+  /// the given [FocusNode].
   ///
-  /// It does not create a rebuild dependency because changing the traversal
-  /// order doesn't change the widget tree, so nothing needs to be rebuilt as a
-  /// result of an order change.
+  /// Will return null if no [FocusTraversalPolicy] ancestor applies to the
+  /// given [FocusNode].
   ///
-  /// Will assert if no [FocusTraversalGroup] ancestor is found.
+  /// The [FocusTraversalPolicy] is set by introducing a [FocusTraversalGroup]
+  /// into the widget tree, which will associate a policy with the focus tree
+  /// under the nearest ancestor [Focus] widget.
+  ///
+  /// This function differs from [maybeOf] in that it takes a [FocusNode] and
+  /// only traverses the focus tree to determine the policy in effect. Unlike
+  /// this function, the [maybeOf] function takes a [BuildContext] and first
+  /// walks up the widget tree to find the nearest ancestor [Focus] or
+  /// [FocusScope] widget, and then calls this function with the focus node
+  /// associated with that widget to determine the policy in effect.
+  static FocusTraversalPolicy? maybeOfNode(FocusNode node) {
+    return _getGroupNode(node)?.policy;
+  }
+
+  static _FocusTraversalGroupNode? _getGroupNode(FocusNode node) {
+    while (node.parent != null) {
+      if (node.context == null) {
+        return null;
+      }
+      if (node is _FocusTraversalGroupNode) {
+        return node;
+      }
+      node = node.parent!;
+    }
+    return null;
+  }
+
+  /// Returns the [FocusTraversalPolicy] that applies to the [FocusNode] of the
+  /// nearest ancestor [Focus] widget, given a [BuildContext].
+  ///
+  /// Will throw a [FlutterError] in debug mode, and throw a null check
+  /// exception in release mode, if no [Focus] ancestor is found, or if no
+  /// [FocusTraversalPolicy] applies to the associated [FocusNode].
+  ///
+  /// {@template flutter.widgets.focus_traversal.FocusTraversalGroup.of}
+  /// This function looks up the nearest ancestor [Focus] (or [FocusScope])
+  /// widget, and uses its [FocusNode] (or [FocusScopeNode]) to walk up the
+  /// focus tree to find the applicable [FocusTraversalPolicy] for that node.
+  ///
+  /// Calling this function does not create a rebuild dependency because
+  /// changing the traversal order doesn't change the widget tree, so nothing
+  /// needs to be rebuilt as a result of an order change.
+  ///
+  /// The [FocusTraversalPolicy] is set by introducing a [FocusTraversalGroup]
+  /// into the widget tree, which will associate a policy with the focus tree
+  /// under the nearest ancestor [Focus] widget.
+  /// {@endtemplate}
   ///
   /// See also:
   ///
-  ///  * [maybeOf] for a similar function that will return null if no
-  ///    [FocusTraversalGroup] ancestor is found.
+  /// * [maybeOf] for a similar function that will return null if no
+  ///   [FocusTraversalGroup] ancestor is found.
+  /// * [maybeOfNode] for a function that will look for a policy using a given
+  ///   [FocusNode], and return null if no policy applies.
   static FocusTraversalPolicy of(BuildContext context) {
-    final _FocusTraversalGroupScope? inherited = context.dependOnInheritedWidgetOfExactType<_FocusTraversalGroupScope>();
+    final FocusTraversalPolicy? policy = maybeOf(context);
     assert(() {
-      if (inherited == null) {
+      if (policy == null) {
         throw FlutterError(
-          'Unable to find a FocusTraversalGroup widget in the context.\n'
+          'Unable to find a Focus or FocusScope widget in the given context, or the FocusNode '
+          'from with the widget that was found is not associated with a FocusTraversalPolicy.\n'
           'FocusTraversalGroup.of() was called with a context that does not contain a '
-          'FocusTraversalGroup.\n'
-          'No FocusTraversalGroup ancestor could be found starting from the context that was '
-          'passed to FocusTraversalGroup.of(). This can happen because there is not a '
-          'WidgetsApp or MaterialApp widget (those widgets introduce a FocusTraversalGroup), '
-          'or it can happen if the context comes from a widget above those widgets.\n'
+          'Focus or FocusScope widget, or there was no FocusTraversalPolicy in effect.\n'
+          'This can happen if there is not a FocusTraversalGroup that defines the policy, '
+          'or if the context comes from a widget that is above the WidgetsApp, MaterialApp, '
+          'or CupertinoApp widget (those widgets introduce an implicit default policy) \n'
           'The context used was:\n'
           '  $context',
         );
       }
       return true;
     }());
-    return inherited!.policy;
+    return policy!;
   }
 
-  /// Returns the focus policy set by the [FocusTraversalGroup] that most
-  /// tightly encloses the given [BuildContext].
+  /// Returns the [FocusTraversalPolicy] that applies to the [FocusNode] of the
+  /// nearest ancestor [Focus] widget, or null, given a [BuildContext].
   ///
-  /// It does not create a rebuild dependency because changing the traversal
-  /// order doesn't change the widget tree, so nothing needs to be rebuilt as a
-  /// result of an order change.
+  /// Will return null if it doesn't find an ancestor [Focus] or [FocusScope]
+  /// widget, or doesn't find a [FocusTraversalPolicy] that applies to the node.
   ///
-  /// Will return null if it doesn't find a [FocusTraversalGroup] ancestor.
+  /// {@macro flutter.widgets.focus_traversal.FocusTraversalGroup.of}
   ///
   /// See also:
   ///
-  ///  * [of] for a similar function that will throw if no [FocusTraversalGroup]
-  ///    ancestor is found.
+  /// * [maybeOfNode] for a similar function that will look for a policy using a
+  ///   given [FocusNode].
+  /// * [of] for a similar function that will throw if no [FocusTraversalPolicy]
+  ///   applies.
   static FocusTraversalPolicy? maybeOf(BuildContext context) {
-    final _FocusTraversalGroupScope? inherited = context.dependOnInheritedWidgetOfExactType<_FocusTraversalGroupScope>();
-    return inherited?.policy;
+    final FocusNode? node = Focus.maybeOf(context, scopeOk: true, createDependency: false);
+    if (node == null) {
+      return null;
+    }
+    return FocusTraversalGroup.maybeOfNode(node);
   }
 
   @override
@@ -1618,21 +1667,28 @@
   }
 }
 
+// A special focus node subclass that only FocusTraversalGroup uses so that it
+// can be used to cache the policy in the focus tree, and so that the traversal
+// code can find groups in the focus tree.
+class _FocusTraversalGroupNode extends FocusNode {
+  _FocusTraversalGroupNode({
+    super.debugLabel,
+    required this.policy,
+  });
+
+  FocusTraversalPolicy policy;
+}
+
 class _FocusTraversalGroupState extends State<FocusTraversalGroup> {
   // The internal focus node used to collect the children of this node into a
   // group, and to provide a context for the traversal algorithm to sort the
-  // group with.
-  late final FocusNode focusNode;
-
-  @override
-  void initState() {
-    super.initState();
-    focusNode = FocusNode(
-      canRequestFocus: false,
-      skipTraversal: true,
-      debugLabel: 'FocusTraversalGroup',
-    );
-  }
+  // group with. It's a special subclass of FocusNode just so that it can be
+  // identified when walking the focus tree during traversal, and hold the
+  // current policy.
+  late final _FocusTraversalGroupNode focusNode = _FocusTraversalGroupNode(
+    debugLabel: 'FocusTraversalGroup',
+    policy: widget.policy,
+  );
 
   @override
   void dispose() {
@@ -1641,36 +1697,25 @@
   }
 
   @override
-  Widget build(BuildContext context) {
-    return _FocusTraversalGroupScope(
-      policy: widget.policy,
-      focusNode: focusNode,
-      child: Focus(
-        focusNode: focusNode,
-        canRequestFocus: false,
-        skipTraversal: true,
-        includeSemantics: false,
-        descendantsAreFocusable: widget.descendantsAreFocusable,
-        descendantsAreTraversable: widget.descendantsAreTraversable,
-        child: widget.child,
-      ),
-    );
+  void didUpdateWidget (FocusTraversalGroup oldWidget) {
+    super.didUpdateWidget(oldWidget);
+    if (oldWidget.policy != widget.policy) {
+      focusNode.policy = widget.policy;
+    }
   }
-}
-
-// A "marker" inherited widget to make the group faster to find.
-class _FocusTraversalGroupScope extends InheritedWidget {
-  const _FocusTraversalGroupScope({
-    required this.policy,
-    required this.focusNode,
-    required super.child,
-  });
-
-  final FocusTraversalPolicy policy;
-  final FocusNode focusNode;
 
   @override
-  bool updateShouldNotify(InheritedWidget oldWidget) => false;
+  Widget build(BuildContext context) {
+    return Focus(
+      focusNode: focusNode,
+      canRequestFocus: false,
+      skipTraversal: true,
+      includeSemantics: false,
+      descendantsAreFocusable: widget.descendantsAreFocusable,
+      descendantsAreTraversable: widget.descendantsAreTraversable,
+      child: widget.child,
+    );
+  }
 }
 
 /// An intent for use with the [RequestFocusAction], which supplies the
diff --git a/framework/lib/src/widgets/framework.dart b/framework/lib/src/widgets/framework.dart
index e576a78..1e6df76 100644
--- a/framework/lib/src/widgets/framework.dart
+++ b/framework/lib/src/widgets/framework.dart
@@ -2216,10 +2216,13 @@
   /// be called apply to this method as well.
   InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object? aspect });
 
-  /// Obtains the nearest widget of the given type `T`, which must be the type of a
-  /// concrete [InheritedWidget] subclass, and registers this build context with
-  /// that widget such that when that widget changes (or a new widget of that
-  /// type is introduced, or the widget goes away), this build context is
+  /// Returns the nearest widget of the given type `T` and creates a dependency
+  /// on it, or null if no appropriate widget is found.
+  ///
+  /// The widget found will be a concrete [InheritedWidget] subclass, and
+  /// calling [dependOnInheritedWidgetOfExactType] registers this build context
+  /// with the returned widget. When that widget changes (or a new widget of
+  /// that type is introduced, or the widget goes away), this build context is
   /// rebuilt so that it can obtain new values from that widget.
   ///
   /// {@template flutter.widgets.BuildContext.dependOnInheritedWidgetOfExactType}
@@ -2230,8 +2233,8 @@
   /// [State.initState] methods, because those methods would not get called
   /// again if the inherited value were to change. To ensure that the widget
   /// correctly updates itself when the inherited value changes, only call this
-  /// (directly or indirectly) from build methods, layout and paint callbacks, or
-  /// from [State.didChangeDependencies].
+  /// (directly or indirectly) from build methods, layout and paint callbacks,
+  /// or from [State.didChangeDependencies].
   ///
   /// This method should not be called from [State.dispose] because the element
   /// tree is no longer stable at that time. To refer to an ancestor from that
@@ -2240,8 +2243,8 @@
   /// whenever the widget is removed from the tree.
   ///
   /// It is also possible to call this method from interaction event handlers
-  /// (e.g. gesture callbacks) or timers, to obtain a value once, if that value
-  /// is not going to be cached and reused later.
+  /// (e.g. gesture callbacks) or timers, to obtain a value once, as long as
+  /// that value is not cached and/or reused later.
   ///
   /// Calling this method is O(1) with a small constant factor, but will lead to
   /// the widget being rebuilt more often.
@@ -2259,6 +2262,27 @@
   /// {@endtemplate}
   T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({ Object? aspect });
 
+  /// Returns the nearest widget of the given [InheritedWidget] subclass `T` or
+  /// null if an appropriate ancestor is not found.
+  ///
+  /// This method does not introduce a dependency the way that the more typical
+  /// [dependOnInheritedWidgetOfExactType] does, so this context will not be
+  /// rebuilt if the [InheritedWidget] changes. This function is meant for those
+  /// uncommon use cases where a dependency is undesirable.
+  ///
+  /// This method should not be called from [State.dispose] because the element
+  /// tree is no longer stable at that time. To refer to an ancestor from that
+  /// method, save a reference to the ancestor in [State.didChangeDependencies].
+  /// It is safe to use this method from [State.deactivate], which is called
+  /// whenever the widget is removed from the tree.
+  ///
+  /// It is also possible to call this method from interaction event handlers
+  /// (e.g. gesture callbacks) or timers, to obtain a value once, as long as
+  /// that value is not cached and/or reused later.
+  ///
+  /// Calling this method is O(1) with a small constant factor.
+  T? getInheritedWidgetOfExactType<T extends InheritedWidget>();
+
   /// Obtains the element corresponding to the nearest widget of the given type `T`,
   /// which must be the type of a concrete [InheritedWidget] subclass.
   ///
@@ -4111,7 +4135,7 @@
       // implementation to decide whether to rebuild based on whether we had
       // dependencies here.
     }
-    _inheritedWidgets = null;
+    _inheritedElements = null;
     _lifecycleState = _ElementLifecycle.inactive;
   }
 
@@ -4306,7 +4330,7 @@
     return null;
   }
 
-  PersistentHashMap<Type, InheritedElement>? _inheritedWidgets;
+  PersistentHashMap<Type, InheritedElement>? _inheritedElements;
   Set<InheritedElement>? _dependencies;
   bool _hadUnsatisfiedDependencies = false;
 
@@ -4347,7 +4371,7 @@
   @override
   T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object? aspect}) {
     assert(_debugCheckStateIsActiveForAncestorLookup());
-    final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T];
+    final InheritedElement? ancestor = _inheritedElements == null ? null : _inheritedElements![T];
     if (ancestor != null) {
       return dependOnInheritedElement(ancestor, aspect: aspect) as T;
     }
@@ -4356,9 +4380,14 @@
   }
 
   @override
+  T? getInheritedWidgetOfExactType<T extends InheritedWidget>() {
+    return getElementForInheritedWidgetOfExactType<T>()?.widget as T?;
+  }
+
+  @override
   InheritedElement? getElementForInheritedWidgetOfExactType<T extends InheritedWidget>() {
     assert(_debugCheckStateIsActiveForAncestorLookup());
-    final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T];
+    final InheritedElement? ancestor = _inheritedElements == null ? null : _inheritedElements![T];
     return ancestor;
   }
 
@@ -4378,7 +4407,7 @@
 
   void _updateInheritance() {
     assert(_lifecycleState == _ElementLifecycle.active);
-    _inheritedWidgets = _parent?._inheritedWidgets;
+    _inheritedElements = _parent?._inheritedElements;
   }
 
   @override
@@ -4672,6 +4701,7 @@
       performRebuild();
     } finally {
       assert(() {
+        owner!._debugElementWasRebuilt(this);
         assert(owner!._debugCurrentBuildTarget == this);
         owner!._debugCurrentBuildTarget = debugPreviousBuildTarget;
         return true;
@@ -5374,8 +5404,8 @@
   void _updateInheritance() {
     assert(_lifecycleState == _ElementLifecycle.active);
     final PersistentHashMap<Type, InheritedElement> incomingWidgets =
-        _parent?._inheritedWidgets ?? const PersistentHashMap<Type, InheritedElement>.empty();
-    _inheritedWidgets = incomingWidgets.put(widget.runtimeType, this);
+        _parent?._inheritedElements ?? const PersistentHashMap<Type, InheritedElement>.empty();
+    _inheritedElements = incomingWidgets.put(widget.runtimeType, this);
   }
 
   @override
@@ -5944,8 +5974,7 @@
     int newChildrenBottom = newWidgets.length - 1;
     int oldChildrenBottom = oldChildren.length - 1;
 
-    final List<Element> newChildren = oldChildren.length == newWidgets.length ?
-        oldChildren : List<Element>.filled(newWidgets.length, _NullElement.instance);
+    final List<Element> newChildren = List<Element>.filled(newWidgets.length, _NullElement.instance);
 
     Element? previousChild;
 
diff --git a/framework/lib/src/widgets/gesture_detector.dart b/framework/lib/src/widgets/gesture_detector.dart
index 21c413b..55dce62 100644
--- a/framework/lib/src/widgets/gesture_detector.dart
+++ b/framework/lib/src/widgets/gesture_detector.dart
@@ -941,7 +941,7 @@
   /// force to initiate a force press. The amount of force is at least
   /// [ForcePressGestureRecognizer.startPressure].
   ///
-  /// Note that this callback will only be fired on devices with pressure
+  /// This callback will only be fired on devices with pressure
   /// detecting screens.
   final GestureForcePressStartCallback? onForcePressStart;
 
@@ -949,7 +949,7 @@
   /// force. The amount of force is at least
   /// [ForcePressGestureRecognizer.peakPressure].
   ///
-  /// Note that this callback will only be fired on devices with pressure
+  /// This callback will only be fired on devices with pressure
   /// detecting screens.
   final GestureForcePressPeakCallback? onForcePressPeak;
 
@@ -958,13 +958,13 @@
   /// plane of the screen, pressing the screen with varying forces or both
   /// simultaneously.
   ///
-  /// Note that this callback will only be fired on devices with pressure
+  /// This callback will only be fired on devices with pressure
   /// detecting screens.
   final GestureForcePressUpdateCallback? onForcePressUpdate;
 
-  /// The pointer is no longer in contact with the screen.
+  /// The pointer tracked by [onForcePressStart] is no longer in contact with the screen.
   ///
-  /// Note that this callback will only be fired on devices with pressure
+  /// This callback will only be fired on devices with pressure
   /// detecting screens.
   final GestureForcePressEndCallback? onForcePressEnd;
 
diff --git a/framework/lib/src/widgets/icon_theme_data.dart b/framework/lib/src/widgets/icon_theme_data.dart
index c1285bd..f0b442b 100644
--- a/framework/lib/src/widgets/icon_theme_data.dart
+++ b/framework/lib/src/widgets/icon_theme_data.dart
@@ -167,6 +167,9 @@
   ///
   /// {@macro dart.ui.shadow.lerp}
   static IconThemeData lerp(IconThemeData? a, IconThemeData? b, double t) {
+    if (identical(a, b) && a != null) {
+      return a;
+    }
     return IconThemeData(
       size: ui.lerpDouble(a?.size, b?.size, t),
       fill: ui.lerpDouble(a?.fill, b?.fill, t),
diff --git a/framework/lib/src/widgets/implicit_animations.dart b/framework/lib/src/widgets/implicit_animations.dart
index 302e865..bf4f37b 100644
--- a/framework/lib/src/widgets/implicit_animations.dart
+++ b/framework/lib/src/widgets/implicit_animations.dart
@@ -185,7 +185,7 @@
     end!.decompose(endTranslation, endRotation, endScale);
     final Vector3 lerpTranslation =
         beginTranslation * (1.0 - t) + endTranslation * t;
-    // TODO(alangardner): Implement slerp for constant rotation
+    // TODO(alangardner): Implement lerp for constant rotation
     final Quaternion lerpRotation =
         (beginRotation.scaled(1.0 - t) + endRotation.scaled(t)).normalized();
     final Vector3 lerpScale = beginScale * (1.0 - t) + endScale * t;
diff --git a/framework/lib/src/widgets/layout_builder.dart b/framework/lib/src/widgets/layout_builder.dart
index be1a907..eed7ce4 100644
--- a/framework/lib/src/widgets/layout_builder.dart
+++ b/framework/lib/src/widgets/layout_builder.dart
@@ -48,7 +48,7 @@
   /// Called at layout time to construct the widget tree.
   ///
   /// The builder must not return null.
-  final Widget Function(BuildContext, ConstraintType) builder;
+  final Widget Function(BuildContext context, ConstraintType constraints) builder;
 
   // updateRenderObject is redundant with the logic in the LayoutBuilderElement below.
 }
diff --git a/framework/lib/src/widgets/media_query.dart b/framework/lib/src/widgets/media_query.dart
index 7757bc9..d91181b 100644
--- a/framework/lib/src/widgets/media_query.dart
+++ b/framework/lib/src/widgets/media_query.dart
@@ -211,10 +211,10 @@
       devicePixelRatio = view.devicePixelRatio,
       textScaleFactor = platformData?.textScaleFactor ?? view.platformDispatcher.textScaleFactor,
       platformBrightness = platformData?.platformBrightness ?? view.platformDispatcher.platformBrightness,
-      padding = EdgeInsets.fromWindowPadding(view.padding, view.devicePixelRatio),
-      viewPadding = EdgeInsets.fromWindowPadding(view.viewPadding, view.devicePixelRatio),
-      viewInsets = EdgeInsets.fromWindowPadding(view.viewInsets, view.devicePixelRatio),
-      systemGestureInsets = EdgeInsets.fromWindowPadding(view.systemGestureInsets, view.devicePixelRatio),
+      padding = EdgeInsets.fromViewPadding(view.padding, view.devicePixelRatio),
+      viewPadding = EdgeInsets.fromViewPadding(view.viewPadding, view.devicePixelRatio),
+      viewInsets = EdgeInsets.fromViewPadding(view.viewInsets, view.devicePixelRatio),
+      systemGestureInsets = EdgeInsets.fromViewPadding(view.systemGestureInsets, view.devicePixelRatio),
       accessibleNavigation = platformData?.accessibleNavigation ?? view.platformDispatcher.accessibilityFeatures.accessibleNavigation,
       invertColors = platformData?.invertColors ?? view.platformDispatcher.accessibilityFeatures.invertColors,
       disableAnimations = platformData?.disableAnimations ?? view.platformDispatcher.accessibilityFeatures.disableAnimations,
@@ -298,7 +298,7 @@
   ///
   /// See also:
   ///
-  ///  * [ui.window], which provides some additional detail about this property
+  ///  * [FlutterView], which provides some additional detail about this property
   ///    and how it relates to [padding] and [viewPadding].
   final EdgeInsets viewInsets;
 
@@ -317,7 +317,7 @@
   ///
   /// See also:
   ///
-  ///  * [ui.window], which provides some additional detail about this
+  ///  * [FlutterView], which provides some additional detail about this
   ///    property and how it relates to [viewInsets] and [viewPadding].
   ///  * [SafeArea], a widget that consumes this padding with a [Padding] widget
   ///    and automatically removes it from the [MediaQuery] for its child.
@@ -341,7 +341,7 @@
   ///
   /// See also:
   ///
-  ///  * [ui.window], which provides some additional detail about this
+  ///  * [FlutterView], which provides some additional detail about this
   ///    property and how it relates to [padding] and [viewInsets].
   final EdgeInsets viewPadding;
 
diff --git a/framework/lib/src/widgets/modal_barrier.dart b/framework/lib/src/widgets/modal_barrier.dart
index 9a15b11..a18a807 100644
--- a/framework/lib/src/widgets/modal_barrier.dart
+++ b/framework/lib/src/widgets/modal_barrier.dart
@@ -53,7 +53,7 @@
 /// Updates the [SemanticsNode.rect] of its child based on the value inside
 /// provided [ValueNotifier].
 class _RenderSemanticsClipper extends RenderProxyBox {
-  /// Creats a [RenderProxyBox] that Updates the [SemanticsNode.rect] of its child
+  /// Creates a [RenderProxyBox] that Updates the [SemanticsNode.rect] of its child
   /// based on the value inside provided [ValueNotifier].
   _RenderSemanticsClipper({
     required ValueNotifier<EdgeInsets> clipDetailsNotifier,
diff --git a/framework/lib/src/widgets/overlay.dart b/framework/lib/src/widgets/overlay.dart
index f1629c9..dfb2351 100644
--- a/framework/lib/src/widgets/overlay.dart
+++ b/framework/lib/src/widgets/overlay.dart
@@ -17,6 +17,8 @@
 // Examples can assume:
 // late BuildContext context;
 
+// * OverlayEntry Implementation
+
 /// A place in an [Overlay] that can contain a widget.
 ///
 /// Overlay entries are inserted into an [Overlay] using the
@@ -127,21 +129,20 @@
   /// Whether the [OverlayEntry] is currently mounted in the widget tree.
   ///
   /// The [OverlayEntry] notifies its listeners when this value changes.
-  bool get mounted => _overlayStateMounted.value;
+  bool get mounted => _overlayEntryStateNotifier.value != null;
 
-  /// Whether the `_OverlayState`s built using this [OverlayEntry] is currently
-  /// mounted.
-  final ValueNotifier<bool> _overlayStateMounted = ValueNotifier<bool>(false);
+  /// The currently mounted `_OverlayEntryWidgetState` built using this [OverlayEntry].
+  final ValueNotifier<_OverlayEntryWidgetState?> _overlayEntryStateNotifier = ValueNotifier<_OverlayEntryWidgetState?>(null);
 
   @override
   void addListener(VoidCallback listener) {
     assert(!_disposedByOwner);
-    _overlayStateMounted.addListener(listener);
+    _overlayEntryStateNotifier.addListener(listener);
   }
 
   @override
   void removeListener(VoidCallback listener) {
-    _overlayStateMounted.removeListener(listener);
+    _overlayEntryStateNotifier.removeListener(listener);
   }
 
   OverlayState? _overlay;
@@ -154,9 +155,9 @@
   /// This method removes this overlay entry from the overlay immediately. The
   /// UI will be updated in the same frame if this method is called before the
   /// overlay rebuild in this frame; otherwise, the UI will be updated in the
-  /// next frame. This means that it is safe to call during builds, but	also
+  /// next frame. This means that it is safe to call during builds, but also
   /// that if you do call this after the overlay rebuild, the UI will not update
-  /// until	the next frame (i.e. many milliseconds later).
+  /// until the next frame (i.e. many milliseconds later).
   void remove() {
     assert(_overlay != null);
     assert(!_disposedByOwner);
@@ -187,7 +188,7 @@
   void _didUnmount() {
     assert(!mounted);
     if (_disposedByOwner) {
-      _overlayStateMounted.dispose();
+      _overlayEntryStateNotifier.dispose();
     }
   }
 
@@ -210,7 +211,7 @@
     assert(_overlay == null, 'An OverlayEntry must first be removed from the Overlay before dispose is called.');
     _disposedByOwner = true;
     if (!mounted) {
-      _overlayStateMounted.dispose();
+      _overlayEntryStateNotifier.dispose();
     }
   }
 
@@ -222,10 +223,12 @@
   const _OverlayEntryWidget({
     required Key key,
     required this.entry,
+    required this.overlayState,
     this.tickerEnabled = true,
   }) : super(key: key);
 
   final OverlayEntry entry;
+  final OverlayState overlayState;
   final bool tickerEnabled;
 
   @override
@@ -233,16 +236,102 @@
 }
 
 class _OverlayEntryWidgetState extends State<_OverlayEntryWidget> {
+  late _RenderTheater _theater;
+
+  // Manages the stack of theater children whose paint order are sorted by their
+  // _zOrderIndex. The children added by OverlayPortal are added to this linked
+  // list, and they will be shown _above_ the OverlayEntry tied to this widget.
+  // The children with larger zOrderIndex values (i.e. those called `show`
+  // recently) will be painted last.
+  //
+  // This linked list is lazily created in `_add`, and the entries are added/removed
+  // via `_add`/`_remove`, called by OverlayPortals lower in the tree. `_add` or
+  // `_remove` does not cause this widget to rebuild, the linked list will be
+  // read by _RenderTheater as part of its render child model. This would ideally
+  // be in a RenderObject but there may not be RenderObjects between
+  // _RenderTheater and the render subtree OverlayEntry builds.
+  LinkedList<_OverlayEntryLocation>? _sortedTheaterSiblings;
+
+  // Worst-case O(N), N being the number of children added to the top spot in
+  // the same frame. This can be a bit expensive when there's a lot of global
+  // key reparenting in the same frame but N is usually a small number.
+  void _add(_OverlayEntryLocation child) {
+    assert(mounted);
+    final LinkedList<_OverlayEntryLocation> children = _sortedTheaterSiblings ??= LinkedList<_OverlayEntryLocation>();
+    assert(!children.contains(child));
+    _OverlayEntryLocation? insertPosition = children.isEmpty ? null : children.last;
+    while (insertPosition != null && insertPosition._zOrderIndex > child._zOrderIndex) {
+      insertPosition = insertPosition.previous;
+    }
+    if (insertPosition == null) {
+      children.addFirst(child);
+    } else {
+      insertPosition.insertAfter(child);
+    }
+    assert(children.contains(child));
+  }
+
+  void _remove(_OverlayEntryLocation child) {
+    assert(_sortedTheaterSiblings != null);
+    final bool wasInCollection = _sortedTheaterSiblings?.remove(child) ?? false;
+    assert(wasInCollection);
+  }
+
+  // Returns an Iterable that traverse the children in the child model in paint
+  // order (from farthest to the user to the closest to the user).
+  //
+  // The iterator should be safe to use even when the child model is being
+  // mutated. The reason for that is it's allowed to add/remove/move deferred
+  // children to a _RenderTheater during performLayout, but the affected
+  // children don't have to be laid out in the same performLayout call.
+  late final Iterable<RenderBox> _paintOrderIterable = _createChildIterable(reversed: false);
+  // An Iterable that traverse the children in the child model in
+  // hit-test order (from closest to the user to the farthest to the user).
+  late final Iterable<RenderBox> _hitTestOrderIterable = _createChildIterable(reversed: true);
+
+  // The following uses sync* because hit-testing is lazy, and LinkedList as a
+  // Iterable doesn't support current modification.
+  Iterable<RenderBox> _createChildIterable({ required bool reversed }) sync* {
+    final LinkedList<_OverlayEntryLocation>? children = _sortedTheaterSiblings;
+    if (children == null || children.isEmpty) {
+      return;
+    }
+    _OverlayEntryLocation? candidate = reversed ? children.last : children.first;
+    while(candidate != null) {
+      final RenderBox? renderBox = candidate._overlayChildRenderBox;
+      candidate = reversed ? candidate.previous : candidate.next;
+      if (renderBox != null) {
+        yield renderBox;
+      }
+    }
+  }
+
   @override
   void initState() {
     super.initState();
-    widget.entry._overlayStateMounted.value = true;
+    widget.entry._overlayEntryStateNotifier.value = this;
+    _theater = context.findAncestorRenderObjectOfType<_RenderTheater>()!;
+    assert(_sortedTheaterSiblings == null);
+  }
+
+  @override
+  void didUpdateWidget(_OverlayEntryWidget oldWidget) {
+    super.didUpdateWidget(oldWidget);
+    // OverlayState's build method always returns a RenderObjectWidget _Theater,
+    // so it's safe to assume that state equality implies render object equality.
+    assert(oldWidget.entry == widget.entry);
+    if (oldWidget.overlayState != widget.overlayState) {
+      final _RenderTheater newTheater = context.findAncestorRenderObjectOfType<_RenderTheater>()!;
+      assert(_theater != newTheater);
+      _theater = newTheater;
+    }
   }
 
   @override
   void dispose() {
-    widget.entry._overlayStateMounted.value = false;
+    widget.entry._overlayEntryStateNotifier.value = null;
     widget.entry._didUnmount();
+    _sortedTheaterSiblings = null;
     super.dispose();
   }
 
@@ -250,7 +339,11 @@
   Widget build(BuildContext context) {
     return TickerMode(
       enabled: widget.tickerEnabled,
-      child: widget.entry.builder(context),
+      child: _RenderTheaterMarker(
+        theater: _theater,
+        overlayEntryWidgetState: this,
+        child: widget.entry.builder(context),
+      ),
     );
   }
 
@@ -594,15 +687,15 @@
   Widget build(BuildContext context) {
     // This list is filled backwards and then reversed below before
     // it is added to the tree.
-    final List<Widget> children = <Widget>[];
+    final List<_OverlayEntryWidget> children = <_OverlayEntryWidget>[];
     bool onstage = true;
     int onstageCount = 0;
-    for (int i = _entries.length - 1; i >= 0; i -= 1) {
-      final OverlayEntry entry = _entries[i];
+    for (final OverlayEntry entry in _entries.reversed) {
       if (onstage) {
         onstageCount += 1;
         children.add(_OverlayEntryWidget(
           key: entry._key,
+          overlayState: this,
           entry: entry,
         ));
         if (entry.opaque) {
@@ -611,12 +704,13 @@
       } else if (entry.maintainState) {
         children.add(_OverlayEntryWidget(
           key: entry._key,
+          overlayState: this,
           entry: entry,
           tickerEnabled: false,
         ));
       }
     }
-    return _Theatre(
+    return _Theater(
       skipCount: children.length - onstageCount,
       clipBehavior: widget.clipBehavior,
       children: children.reversed.toList(growable: false),
@@ -636,11 +730,11 @@
 /// [skipCount] children.
 ///
 /// The first [skipCount] children are considered "offstage".
-class _Theatre extends MultiChildRenderObjectWidget {
-  const _Theatre({
+class _Theater extends MultiChildRenderObjectWidget {
+  const _Theater({
     this.skipCount = 0,
     this.clipBehavior = Clip.hardEdge,
-    super.children,
+    required List<_OverlayEntryWidget> super.children,
   }) : assert(skipCount >= 0),
        assert(children.length >= skipCount);
 
@@ -649,11 +743,11 @@
   final Clip clipBehavior;
 
   @override
-  _TheatreElement createElement() => _TheatreElement(this);
+  _TheaterElement createElement() => _TheaterElement(this);
 
   @override
-  _RenderTheatre createRenderObject(BuildContext context) {
-    return _RenderTheatre(
+  _RenderTheater createRenderObject(BuildContext context) {
+    return _RenderTheater(
       skipCount: skipCount,
       textDirection: Directionality.of(context),
       clipBehavior: clipBehavior,
@@ -661,7 +755,7 @@
   }
 
   @override
-  void updateRenderObject(BuildContext context, _RenderTheatre renderObject) {
+  void updateRenderObject(BuildContext context, _RenderTheater renderObject) {
     renderObject
       ..skipCount = skipCount
       ..textDirection = Directionality.of(context)
@@ -675,22 +769,114 @@
   }
 }
 
-class _TheatreElement extends MultiChildRenderObjectElement {
-  _TheatreElement(_Theatre super.widget);
+class _TheaterElement extends MultiChildRenderObjectElement {
+  _TheaterElement(_Theater super.widget);
 
   @override
-  _RenderTheatre get renderObject => super.renderObject as _RenderTheatre;
+  _RenderTheater get renderObject => super.renderObject as _RenderTheater;
+
+  @override
+  void insertRenderObjectChild(RenderBox child, IndexedSlot<Element?> slot) {
+    super.insertRenderObjectChild(child, slot);
+    final _TheaterParentData parentData = child.parentData! as _TheaterParentData;
+    parentData.overlayEntry = ((widget as _Theater).children[slot.index] as _OverlayEntryWidget).entry;
+    assert(parentData.overlayEntry != null);
+  }
+
+  @override
+  void moveRenderObjectChild(RenderBox child, IndexedSlot<Element?> oldSlot, IndexedSlot<Element?> newSlot) {
+    super.moveRenderObjectChild(child, oldSlot, newSlot);
+    assert(() {
+      final _TheaterParentData parentData = child.parentData! as _TheaterParentData;
+      return parentData.overlayEntry == ((widget as _Theater).children[newSlot.index] as _OverlayEntryWidget).entry;
+    }());
+  }
 
   @override
   void debugVisitOnstageChildren(ElementVisitor visitor) {
-    final _Theatre theatre = widget as _Theatre;
-    assert(children.length >= theatre.skipCount);
-    children.skip(theatre.skipCount).forEach(visitor);
+    final _Theater theater = widget as _Theater;
+    assert(children.length >= theater.skipCount);
+    children.skip(theater.skipCount).forEach(visitor);
   }
 }
 
-class _RenderTheatre extends RenderBox with ContainerRenderObjectMixin<RenderBox, StackParentData> {
-  _RenderTheatre({
+// A `RenderBox` that sizes itself to its parent's size, implements the stack
+// layout algorithm and renders its children in the given `theater`.
+mixin _RenderTheaterMixin on RenderBox {
+  _RenderTheater get theater;
+
+  Iterable<RenderBox> _childrenInPaintOrder();
+  Iterable<RenderBox> _childrenInHitTestOrder();
+
+  @override
+  void setupParentData(RenderBox child) {
+    if (child.parentData is! StackParentData) {
+      child.parentData = StackParentData();
+    }
+  }
+
+  @override
+  bool get sizedByParent => true;
+
+  @override
+  void performLayout() {
+    final Iterator<RenderBox> iterator = _childrenInPaintOrder().iterator;
+    // Same BoxConstraints as used by RenderStack for StackFit.expand.
+    final BoxConstraints nonPositionedChildConstraints = BoxConstraints.tight(constraints.biggest);
+    final Alignment alignment = theater._resolvedAlignment;
+
+    while (iterator.moveNext()) {
+      final RenderBox child = iterator.current;
+      final StackParentData childParentData = child.parentData! as StackParentData;
+      if (!childParentData.isPositioned) {
+        child.layout(nonPositionedChildConstraints, parentUsesSize: true);
+        childParentData.offset = alignment.alongOffset(size - child.size as Offset);
+      } else {
+        assert(child is! _RenderDeferredLayoutBox, 'all _RenderDeferredLayoutBoxes must be non-positioned children.');
+        RenderStack.layoutPositionedChild(child, childParentData, size, alignment);
+      }
+      assert(child.parentData == childParentData);
+    }
+  }
+
+  @override
+  bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
+    final Iterator<RenderBox> iterator = _childrenInHitTestOrder().iterator;
+    bool isHit = false;
+    while (!isHit && iterator.moveNext()) {
+      final RenderBox child = iterator.current;
+      final StackParentData childParentData = child.parentData! as StackParentData;
+      final RenderBox localChild = child;
+      bool childHitTest(BoxHitTestResult result, Offset position) => localChild.hitTest(result, position: position);
+      isHit = result.addWithPaintOffset(offset: childParentData.offset, position: position, hitTest: childHitTest);
+    }
+    return isHit;
+  }
+
+  @override
+  void paint(PaintingContext context, Offset offset) {
+    for (final RenderBox child in _childrenInPaintOrder()) {
+      final StackParentData childParentData = child.parentData! as StackParentData;
+      context.paintChild(child, childParentData.offset + offset);
+    }
+  }
+}
+
+class _TheaterParentData extends StackParentData {
+  // The OverlayEntry that directly created this child. This field is null for
+  // children that are created by an OverlayPortal.
+  OverlayEntry? overlayEntry;
+
+  // _overlayStateMounted is set to null in _OverlayEntryWidgetState's dispose
+  // method. This property is only accessed during layout, paint and hit-test so
+  // the `value!` should be safe.
+  Iterator<RenderBox>? get paintOrderIterator => overlayEntry?._overlayEntryStateNotifier.value!._paintOrderIterable.iterator;
+  Iterator<RenderBox>? get hitTestOrderIterator => overlayEntry?._overlayEntryStateNotifier.value!._hitTestOrderIterable.iterator;
+  void visitChildrenOfOverlayEntry(RenderObjectVisitor visitor) => overlayEntry?._overlayEntryStateNotifier.value!._paintOrderIterable.forEach(visitor);
+}
+
+class _RenderTheater extends RenderBox with ContainerRenderObjectMixin<RenderBox, StackParentData>, _RenderTheaterMixin {
+  _RenderTheater({
     List<RenderBox>? children,
     required TextDirection textDirection,
     int skipCount = 0,
@@ -703,23 +889,52 @@
   }
 
   @override
+  _RenderTheater get theater => this;
+
+  @override
   void setupParentData(RenderBox child) {
-    if (child.parentData is! StackParentData) {
-      child.parentData = StackParentData();
+    if (child.parentData is! _TheaterParentData) {
+      child.parentData = _TheaterParentData();
     }
   }
 
-  Alignment? _resolvedAlignment;
-
-  void _resolve() {
-    if (_resolvedAlignment != null) {
-      return;
+  @override
+  void attach(PipelineOwner owner) {
+    super.attach(owner);
+    RenderBox? child = firstChild;
+    while (child != null) {
+      final _TheaterParentData childParentData = child.parentData! as _TheaterParentData;
+      final Iterator<RenderBox>? iterator = childParentData.paintOrderIterator;
+      if (iterator != null) {
+        while(iterator.moveNext()) {
+          iterator.current.attach(owner);
+        }
+      }
+      child = childParentData.nextSibling;
     }
-    _resolvedAlignment = AlignmentDirectional.topStart.resolve(textDirection);
   }
 
+  static void _detachChild(RenderObject child) => child.detach();
+
+  @override
+  void detach() {
+    super.detach();
+    RenderBox? child = firstChild;
+    while (child != null) {
+      final _TheaterParentData childParentData = child.parentData! as _TheaterParentData;
+      childParentData.visitChildrenOfOverlayEntry(_detachChild);
+      child = childParentData.nextSibling;
+    }
+  }
+
+  @override
+  void redepthChildren() => visitChildren(redepthChild);
+
+  Alignment? _alignmentCache;
+  Alignment get _resolvedAlignment => _alignmentCache ??= AlignmentDirectional.topStart.resolve(textDirection);
+
   void _markNeedResolution() {
-    _resolvedAlignment = null;
+    _alignmentCache = null;
     markNeedsLayout();
   }
 
@@ -755,6 +970,38 @@
     }
   }
 
+  // Adding/removing deferred child does not affect the layout of other children,
+  // or that of the Overlay, so there's no need to invalidate the layout of the
+  // Overlay.
+  //
+  // When _skipMarkNeedsLayout is true, markNeedsLayout does not do anything.
+  bool _skipMarkNeedsLayout = false;
+  void _addDeferredChild(_RenderDeferredLayoutBox child) {
+    assert(!_skipMarkNeedsLayout);
+    _skipMarkNeedsLayout = true;
+
+    adoptChild(child);
+    // When child has never been laid out before, mark its layout surrogate as
+    // needing layout so it's reachable via tree walk.
+    child._layoutSurrogate.markNeedsLayout();
+    _skipMarkNeedsLayout = false;
+  }
+
+  void _removeDeferredChild(_RenderDeferredLayoutBox child) {
+    assert(!_skipMarkNeedsLayout);
+    _skipMarkNeedsLayout = true;
+    dropChild(child);
+    _skipMarkNeedsLayout = false;
+  }
+
+  @override
+  void markNeedsLayout() {
+    if (_skipMarkNeedsLayout) {
+      return;
+    }
+    super.markNeedsLayout();
+  }
+
   RenderBox? get _firstOnstageChild {
     if (skipCount == super.childCount) {
       return null;
@@ -770,8 +1017,6 @@
 
   RenderBox? get _lastOnstageChild => skipCount == super.childCount ? null : lastChild;
 
-  int get _onstageChildCount => childCount - skipCount;
-
   @override
   double computeMinIntrinsicWidth(double height) {
     return RenderStack.getIntrinsicDimension(_firstOnstageChild, (RenderBox child) => child.getMinIntrinsicWidth(height));
@@ -815,73 +1060,49 @@
   }
 
   @override
-  bool get sizedByParent => true;
-
-  @override
   Size computeDryLayout(BoxConstraints constraints) {
     assert(constraints.biggest.isFinite);
     return constraints.biggest;
   }
 
   @override
-  void performLayout() {
-    if (_onstageChildCount == 0) {
-      return;
-    }
-
-    _resolve();
-    assert(_resolvedAlignment != null);
-
-    // Same BoxConstraints as used by RenderStack for StackFit.expand.
-    final BoxConstraints nonPositionedConstraints = BoxConstraints.tight(constraints.biggest);
-
+  // The following uses sync* because concurrent modifications should be allowed
+  // during layout.
+  Iterable<RenderBox> _childrenInPaintOrder() sync* {
     RenderBox? child = _firstOnstageChild;
     while (child != null) {
-      final StackParentData childParentData = child.parentData! as StackParentData;
-
-      if (!childParentData.isPositioned) {
-        child.layout(nonPositionedConstraints, parentUsesSize: true);
-        childParentData.offset = _resolvedAlignment!.alongOffset(size - child.size as Offset);
-      } else {
-        RenderStack.layoutPositionedChild(child, childParentData, size, _resolvedAlignment!);
+      yield child;
+      final _TheaterParentData childParentData = child.parentData! as _TheaterParentData;
+      final Iterator<RenderBox>? innerIterator = childParentData.paintOrderIterator;
+      if (innerIterator != null) {
+        while (innerIterator.moveNext()) {
+          yield innerIterator.current;
+        }
       }
-
-      assert(child.parentData == childParentData);
       child = childParentData.nextSibling;
     }
   }
 
   @override
-  bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
+  // The following uses sync* because hit testing should be lazy.
+  Iterable<RenderBox> _childrenInHitTestOrder() sync* {
     RenderBox? child = _lastOnstageChild;
-    for (int i = 0; i < _onstageChildCount; i++) {
-      assert(child != null);
-      final StackParentData childParentData = child!.parentData! as StackParentData;
-      final bool isHit = result.addWithPaintOffset(
-        offset: childParentData.offset,
-        position: position,
-        hitTest: (BoxHitTestResult result, Offset transformed) {
-          assert(transformed == position - childParentData.offset);
-          return child!.hitTest(result, position: transformed);
-        },
-      );
-      if (isHit) {
-        return true;
+    int childLeft = childCount - skipCount;
+    while (child != null) {
+      final _TheaterParentData childParentData = child.parentData! as _TheaterParentData;
+      final Iterator<RenderBox>? innerIterator = childParentData.hitTestOrderIterator;
+      if (innerIterator != null) {
+        while (innerIterator.moveNext()) {
+          yield innerIterator.current;
+        }
       }
-      child = childParentData.previousSibling;
+      yield child;
+      childLeft -= 1;
+      child = childLeft <= 0 ? null : childParentData.previousSibling;
     }
-    return false;
   }
 
-  @protected
-  void paintStack(PaintingContext context, Offset offset) {
-    RenderBox? child = _firstOnstageChild;
-    while (child != null) {
-      final StackParentData childParentData = child.parentData! as StackParentData;
-      context.paintChild(child, childParentData.offset + offset);
-      child = childParentData.nextSibling;
-    }
-  }
+  final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();
 
   @override
   void paint(PaintingContext context, Offset offset) {
@@ -890,18 +1111,16 @@
         needsCompositing,
         offset,
         Offset.zero & size,
-        paintStack,
+        super.paint,
         clipBehavior: clipBehavior,
         oldLayer: _clipRectLayer.layer,
       );
     } else {
       _clipRectLayer.layer = null;
-      paintStack(context, offset);
+      super.paint(context, offset);
     }
   }
 
-  final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();
-
   @override
   void dispose() {
     _clipRectLayer.layer = null;
@@ -909,11 +1128,23 @@
   }
 
   @override
+  void visitChildren(RenderObjectVisitor visitor) {
+    RenderBox? child = firstChild;
+    while (child != null) {
+      visitor(child);
+      final _TheaterParentData childParentData = child.parentData! as _TheaterParentData;
+      childParentData.visitChildrenOfOverlayEntry(visitor);
+      child = childParentData.nextSibling;
+    }
+  }
+
+  @override
   void visitChildrenForSemantics(RenderObjectVisitor visitor) {
     RenderBox? child = _firstOnstageChild;
     while (child != null) {
       visitor(child);
-      final StackParentData childParentData = child.parentData! as StackParentData;
+      final _TheaterParentData childParentData = child.parentData! as _TheaterParentData;
+      childParentData.visitChildrenOfOverlayEntry(visitor);
       child = childParentData.nextSibling;
     }
   }
@@ -947,6 +1178,7 @@
     RenderBox? child = firstChild;
     final RenderBox? firstOnstageChild = _firstOnstageChild;
     while (child != null) {
+      final _TheaterParentData childParentData = child.parentData! as _TheaterParentData;
       if (child == firstOnstageChild) {
         onstage = true;
         count = 1;
@@ -967,7 +1199,26 @@
         );
       }
 
-      final StackParentData childParentData = child.parentData! as StackParentData;
+      int subcount = 1;
+      childParentData.visitChildrenOfOverlayEntry((RenderObject renderObject) {
+        final RenderBox child = renderObject as RenderBox;
+        if (onstage) {
+          onstageChildren.add(
+            child.toDiagnosticsNode(
+              name: 'onstage $count - $subcount',
+            ),
+          );
+        } else {
+          offstageChildren.add(
+            child.toDiagnosticsNode(
+              name: 'offstage $count - $subcount',
+              style: DiagnosticsTreeStyle.offstage,
+            ),
+          );
+        }
+        subcount += 1;
+      });
+
       child = childParentData.nextSibling;
       count += 1;
     }
@@ -984,3 +1235,876 @@
     ];
   }
 }
+
+
+// * OverlayPortal Implementation
+//  OverlayPortal is inspired by the
+//  [flutter_portal](https://pub.dev/packages/flutter_portal) package.
+//
+// ** RenderObject hierarchy
+// The widget works by inserting its overlay child's render subtree directly
+// under [Overlay]'s render object (_RenderTheater).
+// https://user-images.githubusercontent.com/31859944/171971838-62ed3975-4b5d-4733-a9c9-f79e263b8fcc.jpg
+//
+// To ensure the overlay child render subtree does not do layout twice, the
+// subtree must only perform layout after both its _RenderTheater and the
+// [OverlayPortal]'s render object (_RenderLayoutSurrogateProxyBox) have
+// finished layout. This is handled by _RenderDeferredLayoutBox.
+//
+// ** Z-Index of an overlay child
+// [_OverlayEntryLocation] is a (currently private) interface that allows an
+// [OverlayPortal] to insert its overlay child into a specific [Overlay], as
+// well as specifying the paint order between the overlay child and other
+// children of the _RenderTheater.
+//
+// Since [OverlayPortal] is only allowed to target ancestor [Overlay]s
+// (_RenderTheater must finish doing layout before _RenderDeferredLayoutBox),
+// the _RenderTheater should typically be acquired using an [InheritedWidget]
+// (currently, _RenderTheaterMarker) in case the [OverlayPortal] gets
+// reparented.
+
+/// A class to show, hide and bring to top an [OverlayPortal]'s overlay child
+/// in the target [Overlay].
+///
+/// A [OverlayPortalController] can only be given to at most one [OverlayPortal]
+/// at a time. When an [OverlayPortalController] is moved from one
+/// [OverlayPortal] to another, its [isShowing] state does not carry over.
+///
+/// [OverlayPortalController.show] and [OverlayPortalController.hide] can be
+/// called even before the controller is assigned to any [OverlayPortal], but
+/// they typically should not be called while the widget tree is being rebuilt.
+class OverlayPortalController {
+  /// Creates an [OverlayPortalController], optionally with a String identifier
+  /// `debugLabel`.
+  OverlayPortalController({ String? debugLabel }) : _debugLabel = debugLabel;
+
+  _OverlayPortalState? _attachTarget;
+
+  // A separate _zOrderIndex to allow `show()` or `hide()` to be called when the
+  // controller is not yet attached. Once this controller is attached,
+  // _attachTarget._zOrderIndex will be used as the source of truth, and this
+  // variable will be set to null.
+  int? _zOrderIndex;
+  final String? _debugLabel;
+
+  static int _wallTime = kIsWeb
+    ? -9007199254740992 // -2^53
+    : -1 << 63;
+
+  // Returns a unique and monotonically increasing timestamp that represents
+  // now.
+  //
+  // The value this method returns increments after each call.
+  int _now() {
+    final int now = _wallTime += 1;
+    assert(_zOrderIndex == null || _zOrderIndex! < now);
+    assert(_attachTarget?._zOrderIndex == null || _attachTarget!._zOrderIndex! < now);
+    return now;
+  }
+
+  /// Show the overlay child of the [OverlayPortal] this controller is attached
+  /// to, at the top of the target [Overlay].
+  ///
+  /// When there are more than one [OverlayPortal]s that target the same
+  /// [Overlay], the overlay child of the last [OverlayPortal] to have called
+  /// [show] appears at the top level, unobstructed.
+  ///
+  /// If [isShowing] is already true, calling this method brings the overlay
+  /// child it controls to the top.
+  ///
+  /// This method should typically not be called while the widget tree is being
+  /// rebuilt.
+  void show() {
+    final _OverlayPortalState? state = _attachTarget;
+    if (state != null) {
+      state.show(_now());
+    } else {
+      _zOrderIndex = _now();
+    }
+  }
+
+  /// Hide the [OverlayPortal]'s overlay child.
+  ///
+  /// Once hidden, the overlay child will be removed from the widget tree the
+  /// next time the widget tree rebuilds, and stateful widgets in the overlay
+  /// child may lose states as a result.
+  ///
+  /// This method should typically not be called while the widget tree is being
+  /// rebuilt.
+  void hide() {
+    final _OverlayPortalState? state = _attachTarget;
+    if (state != null) {
+      state.hide();
+    } else {
+      assert(_zOrderIndex != null);
+      _zOrderIndex = null;
+    }
+  }
+
+  /// Whether the associated [OverlayPortal] should build and show its overlay
+  /// child, using its `overlayChildBuilder`.
+  bool get isShowing {
+    final _OverlayPortalState? state = _attachTarget;
+    return state != null
+      ? state._zOrderIndex != null
+      : _zOrderIndex != null;
+  }
+
+  /// Conventience method for toggling the current [isShowing] status.
+  ///
+  /// This method should typically not be called while the widget tree is being
+  /// rebuilt.
+  void toggle() => isShowing ? hide() : show();
+
+  @override
+  String toString() {
+    final String? debugLabel = _debugLabel;
+    final String label = debugLabel == null ? '' : '($debugLabel)';
+    final String isDetached = _attachTarget != null ? '' : ' DETACHED';
+    return '${objectRuntimeType(this, 'OverlayPortalController')}$label$isDetached';
+  }
+}
+
+/// A widget that renders its overlay child on an [Overlay].
+///
+/// The overlay child is initially hidden until [OverlayPortalController.show]
+/// is called on the associated [controller]. The [OverlayPortal] uses
+/// [overlayChildBuilder] to build its overlay child and renders it on the
+/// specified [Overlay] as if it was inserted using an [OverlayEntry], while it
+/// can depend on the same set of [InheritedWidget]s (such as [Theme]) that this
+/// widget can depend on.
+///
+/// This widget requires an [Overlay] ancestor in the widget tree when its
+/// overlay child is showing.
+///
+/// When [OverlayPortalController.hide] is called, the widget built using
+/// [overlayChildBuilder] will be removed from the widget tree the next time the
+/// widget rebuilds. Stateful descendants in the overlay child subtree may lose
+/// states as a result.
+///
+/// {@tool dartpad}
+/// This example uses an [OverlayPortal] to build a tooltip that becomes visible
+/// when the user taps on the [child] widget. There's a [DefaultTextStyle] above
+/// the [OverlayPortal] controlling the [TextStyle] of both the [child] widget
+/// and the widget [overlayChildBuilder] builds, which isn't otherwise doable if
+/// the tooltip was added as an [OverlayEntry].
+///
+/// ** See code in examples/api/lib/widgets/overlay/overlay_portal.0.dart **
+/// {@end-tool}
+///
+/// ### Paint Order
+///
+/// In an [Overlay], an overlay child is painted after the [OverlayEntry]
+/// associated with its [OverlayPortal] (that is, the [OverlayEntry] closest to
+/// the [OverlayPortal] in the widget tree, which usually represents the
+/// enclosing [Route]), and before the next [OverlayEntry].
+///
+/// When an [OverlayEntry] has multiple associated [OverlayPortal]s, the paint
+/// order between their overlay children is the order in which
+/// [OverlayPortalController.show] was called. The last [OverlayPortal] to have
+/// called `show` gets to paint its overlay child in the foreground.
+///
+/// ### Differences between [OverlayPortal] and [OverlayEntry]
+///
+/// The main difference between [OverlayEntry] and [OverlayPortal] is that
+/// [OverlayEntry] builds its widget subtree as a child of the target [Overlay],
+/// while [OverlayPortal] uses [overlayChildBuilder] to build a child widget of
+/// itself. This allows [OverlayPortal]'s overlay child to depend on the same
+/// set of [InheritedWidget]s as [OverlayPortal], and it's also guaranteed that
+/// the overlay child will not outlive its [OverlayPortal].
+///
+/// On the other hand, [OverlayPortal]'s implementation is more complex. For
+/// instance, it does a bit more work than a regular widget during global key
+/// reparenting. If the content to be shown on the [Overlay] doesn't benefit
+/// from being a part of [OverlayPortal]'s subtree, consider using an
+/// [OverlayEntry] instead.
+///
+/// See also:
+///
+///  * [OverlayEntry], an alternative API for inserting widgets into an
+///    [Overlay].
+///  * [Positioned], which can be used to size and position the overlay child in
+///    relation to the target [Overlay]'s boundaries.
+///  * [CompositedTransformFollower], which can be used to position the overlay
+///    child in relation to the linked [CompositedTransformTarget] widget.
+class OverlayPortal extends StatefulWidget {
+  /// Creates an [OverlayPortal] that renders the widget [overlayChildBuilder]
+  /// builds on the closest [Overlay] when [OverlayPortalController.show] is
+  /// called.
+  const OverlayPortal({
+    super.key,
+    required this.controller,
+    required this.overlayChildBuilder,
+    this.child,
+  }) : _targetRootOverlay = false;
+
+  /// Creates an [OverlayPortal] that renders the widget [overlayChildBuilder]
+  /// builds on the root [Overlay] when [OverlayPortalController.show] is
+  /// called.
+  const OverlayPortal.targetsRootOverlay({
+    super.key,
+    required this.controller,
+    required this.overlayChildBuilder,
+    this.child,
+  }) : _targetRootOverlay = true;
+
+  /// The controller to show, hide and bring to top the overlay child.
+  final OverlayPortalController controller;
+
+  /// A [WidgetBuilder] used to build a widget below this widget in the tree,
+  /// that renders on the closest [Overlay].
+  ///
+  /// The said widget will only be built and shown in the closest [Overlay] once
+  /// [OverlayPortalController.show] is called on the associated [controller].
+  /// It will be painted in front of the [OverlayEntry] closest to this widget
+  /// in the widget tree (which is usually the enclosing [Route]).
+  ///
+  /// The built overlay child widget is inserted below this widget in the widget
+  /// tree, allowing it to depend on [InheritedWidget]s above it, and be
+  /// notified when the [InheritedWidget]s change.
+  ///
+  /// Unlike [child], the built overlay child can visually extend outside the
+  /// bounds of this widget without being clipped, and receive hit-test events
+  /// outside of this widget's bounds, as long as it does not extend outside of
+  /// the [Overlay] on which it is rendered.
+  final WidgetBuilder overlayChildBuilder;
+
+  /// A widget below this widget in the tree.
+  final Widget? child;
+
+  final bool _targetRootOverlay;
+
+  @override
+  State<OverlayPortal> createState() => _OverlayPortalState();
+}
+
+class _OverlayPortalState extends State<OverlayPortal> {
+  int? _zOrderIndex;
+  // The location of the overlay child within the overlay. This object will be
+  // used as the slot of the overlay child widget.
+  //
+  // The developer must call `show` to reveal the overlay so we can get a unique
+  // timestamp of the user interaction for sorting.
+  //
+  // Avoid invalidating the cache if possible, since the framework uses `==` to
+  // compare slots, and _OverlayEntryLocation can't override that operator since
+  // it's mutable.
+  bool _childModelMayHaveChanged = true;
+  _OverlayEntryLocation? _locationCache;
+  _OverlayEntryLocation _getLocation(int zOrderIndex, bool targetRootOverlay) {
+    final _OverlayEntryLocation? cachedLocation = _locationCache;
+    if (cachedLocation != null && !_childModelMayHaveChanged) {
+      assert(cachedLocation._zOrderIndex == zOrderIndex);
+      return cachedLocation;
+    }
+    _childModelMayHaveChanged = false;
+    final _RenderTheaterMarker? marker = _RenderTheaterMarker.maybeOf(context, targetRootOverlay: targetRootOverlay);
+    if (marker == null) {
+      throw FlutterError.fromParts(<DiagnosticsNode>[
+        ErrorSummary('No Overlay widget found.'),
+        ErrorDescription(
+          '${widget.runtimeType} widgets require an Overlay widget ancestor.\n'
+          'An overlay lets widgets float on top of other widget children.',
+        ),
+        ErrorHint(
+          'To introduce an Overlay widget, you can either directly '
+          'include one, or use a widget that contains an Overlay itself, '
+          'such as a Navigator, WidgetApp, MaterialApp, or CupertinoApp.',
+        ),
+        ...context.describeMissingAncestor(expectedAncestorType: Overlay),
+      ]);
+    }
+    final _OverlayEntryLocation returnValue;
+    if (cachedLocation == null) {
+      returnValue = _OverlayEntryLocation(zOrderIndex, marker.overlayEntryWidgetState, marker.theater);
+    } else if (cachedLocation._childModel != marker.overlayEntryWidgetState || cachedLocation._theater != marker.theater) {
+      cachedLocation._dispose();
+      returnValue = _OverlayEntryLocation(zOrderIndex, marker.overlayEntryWidgetState, marker.theater);
+    } else {
+      returnValue = cachedLocation;
+    }
+    assert(returnValue._zOrderIndex == zOrderIndex);
+    return _locationCache = returnValue;
+  }
+
+  @override
+  void initState() {
+    super.initState();
+    _setupController(widget.controller);
+  }
+
+  void _setupController(OverlayPortalController controller) {
+    assert(
+      controller._attachTarget == null || controller._attachTarget == this,
+      'Failed to attach $controller to $this. It is already attached to ${controller._attachTarget}.'
+    );
+    final int? controllerZOrderIndex = controller._zOrderIndex;
+    final int? zOrderIndex = _zOrderIndex;
+    if (zOrderIndex == null || (controllerZOrderIndex != null && controllerZOrderIndex > zOrderIndex)) {
+      _zOrderIndex = controllerZOrderIndex;
+    }
+    controller._zOrderIndex = null;
+    controller._attachTarget = this;
+  }
+
+  @override
+  void didChangeDependencies() {
+    super.didChangeDependencies();
+    _childModelMayHaveChanged = true;
+  }
+
+  @override
+  void didUpdateWidget(OverlayPortal oldWidget) {
+    super.didUpdateWidget(oldWidget);
+    _childModelMayHaveChanged = _childModelMayHaveChanged || oldWidget._targetRootOverlay != widget._targetRootOverlay;
+    if (oldWidget.controller != widget.controller) {
+      oldWidget.controller._attachTarget = null;
+      _setupController(widget.controller);
+    }
+  }
+
+  @override
+  void dispose() {
+    widget.controller._attachTarget = null;
+    _locationCache?._dispose();
+    _locationCache = null;
+    super.dispose();
+  }
+
+  void show(int zOrderIndex) {
+    assert(
+      SchedulerBinding.instance.schedulerPhase != SchedulerPhase.persistentCallbacks,
+      '${widget.controller.runtimeType}.show() should not be called during build.'
+    );
+    setState(() { _zOrderIndex = zOrderIndex; });
+    _locationCache?._dispose();
+    _locationCache = null;
+  }
+
+  void hide() {
+    assert(SchedulerBinding.instance.schedulerPhase != SchedulerPhase.persistentCallbacks);
+    setState(() { _zOrderIndex = null; });
+    _locationCache?._dispose();
+    _locationCache = null;
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final int? zOrderIndex = _zOrderIndex;
+    if (zOrderIndex == null) {
+      return _OverlayPortal(
+        overlayLocation: null,
+        overlayChild: null,
+        child: widget.child,
+      );
+    }
+    return _OverlayPortal(
+      overlayLocation: _getLocation(zOrderIndex, widget._targetRootOverlay),
+      overlayChild: _DeferredLayout(child: Builder(builder: widget.overlayChildBuilder)),
+      child: widget.child,
+    );
+  }
+}
+
+/// A location in an [Overlay].
+///
+/// An [_OverlayEntryLocation] determines the [Overlay] the associated
+/// [OverlayPortal] should put its overlay child onto, as well as the overlay
+/// child's paint order in relation to other contents painted on the [Overlay].
+//
+// An _OverlayEntryLocation is a cursor pointing to a location in a particular
+// Overlay's child model, and provides methods to insert/remove/move a
+// _RenderDeferredLayoutBox to/from its target _theater.
+//
+// The occupant (a `RenderBox`) will be painted above the associated
+// [OverlayEntry], but below the [OverlayEntry] above that [OverlayEntry].
+//
+// Additionally, `_activate` and `_deactivate` are called when the overlay
+// child's `_OverlayPortalElement` activates/deactivates (for instance, during
+// global key reparenting).
+// `_OverlayPortalElement` removes its overlay child's render object from the
+// target `_RenderTheater` when it deactivates and puts it back on `activated`.
+// These 2 methods can be used to "hide" a child in the child model without
+// removing it, when the child is expensive/difficult to re-insert at the
+// correct location on `activated`.
+//
+// ### Equality
+//
+// An `_OverlayEntryLocation` will be used as an Element's slot. These 3 parts
+// uniquely identify a place in an overlay's child model:
+// - _theater
+// - _childModel (the OverlayEntry)
+// - _zOrderIndex
+//
+// Since it can't implement operator== (it's mutable), the same `_OverlayEntryLocation`
+// instance must not be used to represent more than one locations.
+class _OverlayEntryLocation extends LinkedListEntry<_OverlayEntryLocation> {
+  _OverlayEntryLocation(this._zOrderIndex, this._childModel, this._theater);
+
+  final int _zOrderIndex;
+  final _OverlayEntryWidgetState _childModel;
+  final _RenderTheater _theater;
+
+  _RenderDeferredLayoutBox? _overlayChildRenderBox;
+  void _addToChildModel(_RenderDeferredLayoutBox child) {
+    assert(_overlayChildRenderBox == null, 'Failed to add $child. This location ($this) is already occupied by $_overlayChildRenderBox.');
+    _overlayChildRenderBox = child;
+    _childModel._add(this);
+    _theater.markNeedsPaint();
+    _theater.markNeedsCompositingBitsUpdate();
+    _theater.markNeedsSemanticsUpdate();
+  }
+  void _removeFromChildModel(_RenderDeferredLayoutBox child) {
+    assert(child == _overlayChildRenderBox);
+    _overlayChildRenderBox = null;
+    assert(_childModel._sortedTheaterSiblings?.contains(this) ?? false);
+    _childModel._remove(this);
+    _theater.markNeedsPaint();
+    _theater.markNeedsCompositingBitsUpdate();
+    _theater.markNeedsSemanticsUpdate();
+  }
+
+  void _addChild(_RenderDeferredLayoutBox child) {
+    assert(_debugNotDisposed());
+    _addToChildModel(child);
+    _theater._addDeferredChild(child);
+    assert(child.parent == _theater);
+  }
+
+  void _removeChild(_RenderDeferredLayoutBox child) {
+    // This call is allowed even when this location is disposed.
+    _removeFromChildModel(child);
+    _theater._removeDeferredChild(child);
+    assert(child.parent == null);
+  }
+
+  void _moveChild(_RenderDeferredLayoutBox child, _OverlayEntryLocation fromLocation) {
+    assert(fromLocation != this);
+    assert(_debugNotDisposed());
+    final _RenderTheater fromTheater = fromLocation._theater;
+    final _OverlayEntryWidgetState fromModel = fromLocation._childModel;
+
+    if (fromTheater != _theater) {
+      fromTheater._removeDeferredChild(child);
+      _theater._addDeferredChild(child);
+    }
+
+    if (fromModel != _childModel || fromLocation._zOrderIndex != _zOrderIndex) {
+      fromLocation._removeFromChildModel(child);
+      _addToChildModel(child);
+    }
+  }
+
+  void _activate(_RenderDeferredLayoutBox child) {
+    assert(_debugNotDisposed());
+    assert(_overlayChildRenderBox == null, '$_overlayChildRenderBox');
+    _theater.adoptChild(child);
+    _overlayChildRenderBox = child;
+  }
+
+  void _deactivate(_RenderDeferredLayoutBox child) {
+    assert(_debugNotDisposed());
+    _theater.dropChild(child);
+    _overlayChildRenderBox = null;
+  }
+
+  bool _debugNotDisposed() {
+    if (_debugDisposedStackTrace == null) {
+      return true;
+    }
+    throw StateError('$this is already disposed. Stack trace: $_debugDisposedStackTrace');
+  }
+
+  StackTrace? _debugDisposedStackTrace;
+  @mustCallSuper
+  void _dispose() {
+    assert(_debugNotDisposed());
+    assert(() {
+      _debugDisposedStackTrace = StackTrace.current;
+      return true;
+    }());
+  }
+}
+
+class _RenderTheaterMarker extends InheritedWidget {
+  const _RenderTheaterMarker({
+    required this.theater,
+    required this.overlayEntryWidgetState,
+    required super.child,
+  });
+
+  final _RenderTheater theater;
+  final _OverlayEntryWidgetState overlayEntryWidgetState;
+
+  @override
+  bool updateShouldNotify(_RenderTheaterMarker oldWidget) {
+    return oldWidget.theater != theater
+        || oldWidget.overlayEntryWidgetState != overlayEntryWidgetState;
+  }
+
+  static _RenderTheaterMarker? maybeOf(BuildContext context, { bool targetRootOverlay = false }) {
+    if (targetRootOverlay) {
+      final InheritedElement? ancestor = _rootRenderTheaterMarkerOf(context.getElementForInheritedWidgetOfExactType<_RenderTheaterMarker>());
+      assert(ancestor == null || ancestor.widget is _RenderTheaterMarker);
+      return ancestor != null ? context.dependOnInheritedElement(ancestor) as _RenderTheaterMarker? : null;
+    }
+    return context.dependOnInheritedWidgetOfExactType<_RenderTheaterMarker>();
+  }
+
+  static InheritedElement? _rootRenderTheaterMarkerOf(InheritedElement? theaterMarkerElement) {
+    assert(theaterMarkerElement == null || theaterMarkerElement.widget is _RenderTheaterMarker);
+    if (theaterMarkerElement == null) {
+      return null;
+    }
+    InheritedElement? ancestor;
+    theaterMarkerElement.visitAncestorElements((Element element) {
+      ancestor = element.getElementForInheritedWidgetOfExactType<_RenderTheaterMarker>();
+      return false;
+    });
+    return ancestor == null ? theaterMarkerElement : _rootRenderTheaterMarkerOf(ancestor);
+  }
+}
+
+class _OverlayPortal extends RenderObjectWidget {
+  /// Creates a widget that renders the given [overlayChild] in the [Overlay]
+  /// specified by `overlayLocation`.
+  ///
+  /// The `overlayLocation` parameter must not be null when [overlayChild] is not
+  /// null.
+  _OverlayPortal({
+    required this.overlayLocation,
+    required this.overlayChild,
+    required this.child,
+  }) : assert(overlayChild == null || overlayLocation != null),
+       assert(overlayLocation == null || overlayLocation._debugNotDisposed());
+
+  final Widget? overlayChild;
+
+  /// A widget below this widget in the tree.
+  final Widget? child;
+
+  final _OverlayEntryLocation? overlayLocation;
+
+  @override
+  RenderObjectElement createElement() => _OverlayPortalElement(this);
+
+  @override
+  RenderObject createRenderObject(BuildContext context) => _RenderLayoutSurrogateProxyBox();
+}
+
+class _OverlayPortalElement extends RenderObjectElement {
+  _OverlayPortalElement(_OverlayPortal super.widget);
+
+  @override
+  _RenderLayoutSurrogateProxyBox get renderObject => super.renderObject as _RenderLayoutSurrogateProxyBox;
+
+  Element? _overlayChild;
+  Element? _child;
+
+  @override
+  void mount(Element? parent, Object? newSlot) {
+    super.mount(parent, newSlot);
+    final _OverlayPortal widget = this.widget as _OverlayPortal;
+    _child = updateChild(_child, widget.child, null);
+    _overlayChild = updateChild(_overlayChild, widget.overlayChild, widget.overlayLocation);
+  }
+
+  @override
+  void update(_OverlayPortal newWidget) {
+    super.update(newWidget);
+    _child = updateChild(_child, newWidget.child, null);
+    _overlayChild = updateChild(_overlayChild, newWidget.overlayChild, newWidget.overlayLocation);
+  }
+
+  @override
+  void forgetChild(Element child) {
+    // The _overlayChild Element does not have a key because the _DeferredLayout
+    // widget does not take a Key, so only the regular _child can be taken
+    // during global key reparenting.
+    assert(child == _child);
+    _child = null;
+    super.forgetChild(child);
+  }
+
+  @override
+  void visitChildren(ElementVisitor visitor) {
+    final Element? child = _child;
+    final Element? overlayChild = _overlayChild;
+    if (child != null) {
+      visitor(child);
+    }
+    if (overlayChild != null) {
+      visitor(overlayChild);
+    }
+  }
+
+  @override
+  void activate() {
+    super.activate();
+    final Element? overlayChild = _overlayChild;
+    if (overlayChild != null) {
+      final _RenderDeferredLayoutBox? box = overlayChild.renderObject as _RenderDeferredLayoutBox?;
+      if (box != null) {
+        assert(!box.attached);
+        assert(renderObject._deferredLayoutChild == box);
+        (overlayChild.slot! as _OverlayEntryLocation)._activate(box);
+      }
+    }
+  }
+
+  @override
+  void deactivate() {
+    final Element? overlayChild = _overlayChild;
+    // Instead of just detaching the render objects, removing them from the
+    // render subtree entirely such that if the widget gets reparented to a
+    // different overlay entry, the overlay child is inserted in the right
+    // position in the overlay's child list.
+    //
+    // This is also a workaround for the !renderObject.attached assert in the
+    // `RenderObjectElement.deactive()` method.
+    if (overlayChild != null) {
+      final _RenderDeferredLayoutBox? box = overlayChild.renderObject as _RenderDeferredLayoutBox?;
+      if (box != null) {
+        (overlayChild.slot! as _OverlayEntryLocation)._deactivate(box);
+      }
+    }
+    super.deactivate();
+  }
+
+  @override
+  void insertRenderObjectChild(RenderBox child, _OverlayEntryLocation? slot) {
+    assert(child.parent == null, "$child's parent is not null: ${child.parent}");
+    if (slot != null) {
+      renderObject._deferredLayoutChild = child as _RenderDeferredLayoutBox;
+      slot._addChild(child);
+    } else {
+      renderObject.child = child;
+    }
+  }
+
+  // The [_DeferredLayout] widget does not have a key so there will be no
+  // reparenting between _overlayChild and _child, thus the non-null-typed slots.
+  @override
+  void moveRenderObjectChild(_RenderDeferredLayoutBox child, _OverlayEntryLocation oldSlot, _OverlayEntryLocation newSlot) {
+    assert(newSlot._debugNotDisposed());
+    newSlot._moveChild(child, oldSlot);
+  }
+
+  @override
+  void removeRenderObjectChild(RenderBox child, _OverlayEntryLocation? slot) {
+    if (slot == null) {
+      renderObject.child = null;
+      return;
+    }
+    assert(renderObject._deferredLayoutChild == child);
+    slot._removeChild(child as _RenderDeferredLayoutBox);
+    renderObject._deferredLayoutChild = null;
+  }
+
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    properties.add(DiagnosticsProperty<Element>('child', _child, defaultValue: null));
+    properties.add(DiagnosticsProperty<Element>('overlayChild', _overlayChild, defaultValue: null));
+    properties.add(DiagnosticsProperty<Object>('overlayLocation', _overlayChild?.slot, defaultValue: null));
+  }
+}
+
+class _DeferredLayout extends SingleChildRenderObjectWidget {
+  const _DeferredLayout({
+    // This widget must not be given a key: we currently do not support
+    // reparenting between the overlayChild and child.
+    required Widget child,
+  }) : super(child: child);
+
+  _RenderLayoutSurrogateProxyBox getLayoutParent(BuildContext context) {
+    return context.findAncestorRenderObjectOfType<_RenderLayoutSurrogateProxyBox>()!;
+  }
+
+  @override
+  _RenderDeferredLayoutBox createRenderObject(BuildContext context) {
+    final _RenderLayoutSurrogateProxyBox parent = getLayoutParent(context);
+    final _RenderDeferredLayoutBox renderObject = _RenderDeferredLayoutBox(parent);
+    parent._deferredLayoutChild = renderObject;
+    return renderObject;
+  }
+
+  @override
+  void updateRenderObject(BuildContext context, _RenderDeferredLayoutBox renderObject) {
+    assert(renderObject._layoutSurrogate == getLayoutParent(context));
+    assert(getLayoutParent(context)._deferredLayoutChild == renderObject);
+  }
+}
+
+// A `RenderProxyBox` that defers its layout until its `_layoutSurrogate` is
+// laid out.
+//
+// This `RenderObject` must be a child of a `_RenderTheater`. It guarantees that:
+//
+// 1. It's a relayout boundary, and `markParentNeedsLayout` is overridden such
+//    that it never dirties its `_RenderTheater`.
+//
+// 2. Its `layout` implementation is overridden such that `performLayout` does
+//    not do anything when its called from `layout`, preventing the parent
+//    `_RenderTheater` from laying out this subtree prematurely (but this
+//    `RenderObject` may still be resized). Instead, `markNeedsLayout` will be
+//    called from within `layout` to schedule a layout update for this relayout
+//    boundary when needed.
+//
+// 3. When invoked from `PipelineOwner.flushLayout`, or
+//    `_layoutSurrogate.performLayout`, this `RenderObject` behaves like an
+//    `Overlay` that has only one entry.
+class _RenderDeferredLayoutBox extends RenderProxyBox with _RenderTheaterMixin, LinkedListEntry<_RenderDeferredLayoutBox> {
+  _RenderDeferredLayoutBox(this._layoutSurrogate);
+
+  StackParentData get stackParentData => parentData! as StackParentData;
+  final _RenderLayoutSurrogateProxyBox _layoutSurrogate;
+
+  @override
+  Iterable<RenderBox> _childrenInPaintOrder() {
+    final RenderBox? child = this.child;
+    return child == null
+      ? const Iterable<RenderBox>.empty()
+      : Iterable<RenderBox>.generate(1, (int i) => child);
+  }
+  @override
+  Iterable<RenderBox> _childrenInHitTestOrder() => _childrenInPaintOrder();
+
+  @override
+  _RenderTheater get theater {
+    final AbstractNode? parent = this.parent;
+    return parent is _RenderTheater
+      ? parent
+      : throw FlutterError('$parent of $this is not a _RenderTheater');
+  }
+
+  @override
+  void redepthChildren() {
+    _layoutSurrogate.redepthChild(this);
+    super.redepthChildren();
+  }
+
+  bool _callingMarkParentNeedsLayout = false;
+  @override
+  void markParentNeedsLayout() {
+    // No re-entrant calls.
+    if (_callingMarkParentNeedsLayout) {
+      return;
+    }
+    _callingMarkParentNeedsLayout = true;
+    markNeedsLayout();
+    _layoutSurrogate.markNeedsLayout();
+    _callingMarkParentNeedsLayout = false;
+  }
+
+  bool _needsLayout = true;
+  @override
+  void markNeedsLayout() {
+    _needsLayout = true;
+    super.markNeedsLayout();
+  }
+
+  @override
+  RenderObject? get debugLayoutParent => _layoutSurrogate;
+
+  void layoutByLayoutSurrogate() {
+    assert(!_parentDoingLayout);
+    final _RenderTheater? theater = parent as _RenderTheater?;
+    if (theater == null || !attached) {
+      assert(false, '$this is not attached to parent');
+      return;
+    }
+    super.layout(BoxConstraints.tight(theater.constraints.biggest));
+  }
+
+  bool _parentDoingLayout = false;
+  @override
+  void layout(Constraints constraints, { bool parentUsesSize = false }) {
+    assert(_needsLayout == debugNeedsLayout);
+    // Only _RenderTheater calls this implementation.
+    assert(parent != null);
+    final bool scheduleDeferredLayout = _needsLayout || this.constraints != constraints;
+    assert(!_parentDoingLayout);
+    _parentDoingLayout = true;
+    super.layout(constraints, parentUsesSize: parentUsesSize);
+    assert(_parentDoingLayout);
+    _parentDoingLayout = false;
+    _needsLayout = false;
+    assert(!debugNeedsLayout);
+    if (scheduleDeferredLayout) {
+      final _RenderTheater parent = this.parent! as _RenderTheater;
+      // Invoking markNeedsLayout as a layout callback allows this node to be
+      // merged back to the `PipelineOwner` if it's not already dirty. Otherwise
+      // this may cause some dirty descendants to performLayout a second time.
+      parent.invokeLayoutCallback((BoxConstraints constraints) { markNeedsLayout(); });
+    }
+  }
+
+  @override
+  void performResize() {
+    size = constraints.biggest;
+  }
+
+  bool _debugMutationsLocked = false;
+  @override
+  void performLayout() {
+    assert(!_debugMutationsLocked);
+    if (_parentDoingLayout) {
+      _needsLayout = false;
+      return;
+    }
+    assert(() {
+      _debugMutationsLocked = true;
+      return true;
+    }());
+    // This method is directly being invoked from `PipelineOwner.flushLayout`,
+    // or from `_layoutSurrogate`'s performLayout.
+    assert(parent != null);
+    final RenderBox? child = this.child;
+    if (child == null) {
+      _needsLayout = false;
+      return;
+    }
+    super.performLayout();
+    assert(() {
+      _debugMutationsLocked = false;
+      return true;
+    }());
+    _needsLayout = false;
+  }
+
+  @override
+  void applyPaintTransform(RenderBox child, Matrix4 transform) {
+    final BoxParentData childParentData = child.parentData! as BoxParentData;
+    final Offset offset = childParentData.offset;
+    transform.translate(offset.dx, offset.dy);
+  }
+}
+
+// A RenderProxyBox that makes sure its `deferredLayoutChild` has a greater
+// depth than itself.
+class _RenderLayoutSurrogateProxyBox extends RenderProxyBox {
+  _RenderDeferredLayoutBox? _deferredLayoutChild;
+
+  @override
+  void redepthChildren() {
+    super.redepthChildren();
+    final _RenderDeferredLayoutBox? child = _deferredLayoutChild;
+    // If child is not attached, this method will be invoked by child's real
+    // parent when it's attached.
+    if (child != null && child.attached) {
+      assert(child.attached);
+      redepthChild(child);
+    }
+  }
+
+  @override
+  void performLayout() {
+    super.performLayout();
+    // Try to layout `_deferredLayoutChild` here now that its configuration
+    // and constraints are up-to-date. Additionally, during the very first
+    // layout, this makes sure that _deferredLayoutChild is reachable via tree
+    // walk.
+    _deferredLayoutChild?.layoutByLayoutSurrogate();
+  }
+}
diff --git a/framework/lib/src/widgets/overscroll_indicator.dart b/framework/lib/src/widgets/overscroll_indicator.dart
index 272d089..479ec56 100644
--- a/framework/lib/src/widgets/overscroll_indicator.dart
+++ b/framework/lib/src/widgets/overscroll_indicator.dart
@@ -201,6 +201,11 @@
     if (!widget.notificationPredicate(notification)) {
       return false;
     }
+    if (notification.metrics.axis != widget.axis) {
+      // This widget is explicitly configured to one axis. If a notification
+      // from a different axis bubbles up, do nothing.
+      return false;
+    }
 
     // Update the paint offset with the current scroll position. This makes
     // sure that the glow effect correctly scrolls in line with the current
@@ -236,7 +241,6 @@
         }
       }
       assert(controller != null);
-      assert(notification.metrics.axis == widget.axis);
       if (_accepted[isLeading]!) {
         if (notification.velocity != 0.0) {
           assert(notification.dragDetails == null);
@@ -707,6 +711,11 @@
     if (!widget.notificationPredicate(notification)) {
       return false;
     }
+    if (notification.metrics.axis != widget.axis) {
+      // This widget is explicitly configured to one axis. If a notification
+      // from a different axis bubbles up, do nothing.
+      return false;
+    }
 
     if (notification is OverscrollNotification) {
       _lastOverscrollNotification = notification;
@@ -716,7 +725,6 @@
         _accepted = confirmationNotification.accepted;
       }
 
-      assert(notification.metrics.axis == widget.axis);
       if (_accepted) {
         _totalOverscroll += notification.overscroll;
 
diff --git a/framework/lib/src/widgets/platform_menu_bar.dart b/framework/lib/src/widgets/platform_menu_bar.dart
index 9127760..ccbc7c5 100644
--- a/framework/lib/src/widgets/platform_menu_bar.dart
+++ b/framework/lib/src/widgets/platform_menu_bar.dart
@@ -738,7 +738,8 @@
   /// An optional callback that is called when this [PlatformMenuItem] is
   /// selected.
   ///
-  /// If unset, this menu item will be disabled.
+  /// At most one of [onSelected] and [onSelectedIntent] may be set. If neither
+  /// field is set, this menu item will be disabled.
   final VoidCallback? onSelected;
 
   /// Returns a callback, if any, to be invoked if the platform menu receives a
@@ -760,7 +761,8 @@
   /// An optional intent that is invoked when this [PlatformMenuItem] is
   /// selected.
   ///
-  /// If unset, this menu item will be disabled.
+  /// At most one of [onSelected] and [onSelectedIntent] may be set. If neither
+  /// field is set, this menu item will be disabled.
   final Intent? onSelectedIntent;
 
   /// Returns all descendant [PlatformMenuItem]s of this item.
@@ -805,7 +807,7 @@
     return <String, Object?>{
       _kIdKey: getId(item),
       _kLabelKey: item.label,
-      _kEnabledKey: item.onSelected != null,
+      _kEnabledKey: item.onSelected != null || item.onSelectedIntent != null,
       if (shortcut != null)...shortcut.serializeForMenu().toChannelRepresentation(),
     };
   }
diff --git a/framework/lib/src/widgets/platform_view.dart b/framework/lib/src/widgets/platform_view.dart
index 2513191..bf3fbb6 100644
--- a/framework/lib/src/widgets/platform_view.dart
+++ b/framework/lib/src/widgets/platform_view.dart
@@ -1088,7 +1088,7 @@
   void initState() {
     super.initState();
     if (!widget.controller.isCreated) {
-      // Schedule a rebuild once creation is complete and the final dislay
+      // Schedule a rebuild once creation is complete and the final display
       // type is known.
       widget.controller.addOnPlatformViewCreatedListener(_onPlatformViewCreated);
     }
diff --git a/framework/lib/src/widgets/scroll_metrics.dart b/framework/lib/src/widgets/scroll_metrics.dart
index e637e0f..cf11802 100644
--- a/framework/lib/src/widgets/scroll_metrics.dart
+++ b/framework/lib/src/widgets/scroll_metrics.dart
@@ -62,7 +62,7 @@
   ///
   /// The actual [pixels] value might be [outOfRange].
   ///
-  /// This value should typically be non-null and less than or equal to
+  /// This value is typically less than or equal to
   /// [maxScrollExtent]. It can be negative infinity, if the scroll is unbounded.
   double get minScrollExtent;
 
@@ -70,7 +70,7 @@
   ///
   /// The actual [pixels] value might be [outOfRange].
   ///
-  /// This value should typically be non-null and greater than or equal to
+  /// This value is typically greater than or equal to
   /// [minScrollExtent]. It can be infinity, if the scroll is unbounded.
   double get maxScrollExtent;
 
diff --git a/framework/lib/src/widgets/scroll_physics.dart b/framework/lib/src/widgets/scroll_physics.dart
index 350a298..77c3a69 100644
--- a/framework/lib/src/widgets/scroll_physics.dart
+++ b/framework/lib/src/widgets/scroll_physics.dart
@@ -365,6 +365,28 @@
   /// The given `position` is only valid during this method call. Do not keep a
   /// reference to it to use later, as the values may update, may not update, or
   /// may update to reflect an entirely unrelated scrollable.
+  ///
+  /// This method can potentially be called in every frame, even in the middle
+  /// of what the user perceives as a single ballistic scroll.  For example, in
+  /// a [ListView] when previously off-screen items come into view and are laid
+  /// out, this method may be called with a new [ScrollMetrics.maxScrollExtent].
+  /// The method implementation should ensure that when the same ballistic
+  /// scroll motion is still intended, these calls have no side effects on the
+  /// physics beyond continuing that motion.
+  ///
+  /// Generally this is ensured by having the [Simulation] conform to a physical
+  /// metaphor of a particle in ballistic flight, where the forces on the
+  /// particle depend only on its position, velocity, and environment, and not
+  /// on the current time or any internal state.  This means that the
+  /// time-derivative of [Simulation.dx] should be possible to write
+  /// mathematically as a function purely of the values of [Simulation.x],
+  /// [Simulation.dx], and the parameters used to construct the [Simulation],
+  /// independent of the time.
+  // TODO(gnprice): Some scroll physics in the framework violate that invariant; fix them.
+  //   An audit found three cases violating the invariant:
+  //     https://github.com/flutter/flutter/issues/120338
+  //     https://github.com/flutter/flutter/issues/120340
+  //     https://github.com/flutter/flutter/issues/109675
   Simulation? createBallisticSimulation(ScrollMetrics position, double velocity) {
     if (parent == null) {
       return null;
diff --git a/framework/lib/src/widgets/scroll_simulation.dart b/framework/lib/src/widgets/scroll_simulation.dart
index 5e384c4..d377958 100644
--- a/framework/lib/src/widgets/scroll_simulation.dart
+++ b/framework/lib/src/widgets/scroll_simulation.dart
@@ -123,98 +123,129 @@
   }
 }
 
-/// An implementation of scroll physics that matches Android.
+/// An implementation of scroll physics that aligns with Android.
+///
+/// For any value of [velocity], this travels the same total distance as the
+/// Android scroll physics.
+///
+/// This scroll physics has been adjusted relative to Android's in order to make
+/// it ballistic, meaning that the deceleration at any moment is a function only
+/// of the current velocity [dx] and does not depend on how long ago the
+/// simulation was started.  (This is required by Flutter's scrolling protocol,
+/// where [ScrollActivityDelegate.goBallistic] may restart a scroll activity
+/// using only its current velocity and the scroll position's own state.)
+/// Compared to this scroll physics, Android's moves faster at the very
+/// beginning, then slower, and it ends at the same place but a little later.
+///
+/// Times are measured in seconds, and positions in logical pixels.
 ///
 /// See also:
 ///
 ///  * [BouncingScrollSimulation], which implements iOS scroll physics.
 //
-// This class is based on Scroller.java from Android:
-//   https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/widget
+// This class is based on OverScroller.java from Android:
+//   https://android.googlesource.com/platform/frameworks/base/+/android-13.0.0_r24/core/java/android/widget/OverScroller.java#738
+// and in particular class SplineOverScroller (at the end of the file), starting
+// at method "fling".  (A very similar algorithm is in Scroller.java in the same
+// directory, but OverScroller is what's used by RecyclerView.)
 //
-// The "See..." comments below refer to Scroller methods and values. Some
-// simplifications have been made.
+// In the Android implementation, times are in milliseconds, positions are in
+// physical pixels, but velocity is in physical pixels per whole second.
+//
+// The "See..." comments below refer to SplineOverScroller methods and values.
 class ClampingScrollSimulation extends Simulation {
-  /// Creates a scroll physics simulation that matches Android scrolling.
+  /// Creates a scroll physics simulation that aligns with Android scrolling.
   ClampingScrollSimulation({
     required this.position,
     required this.velocity,
     this.friction = 0.015,
     super.tolerance,
-  }) : assert(_flingVelocityPenetration(0.0) == _initialVelocityPenetration) {
-    _duration = _flingDuration(velocity);
-    _distance = (velocity * _duration / _initialVelocityPenetration).abs();
+  }) {
+    _duration = _flingDuration();
+    _distance = _flingDistance();
   }
 
-  /// The position of the particle at the beginning of the simulation.
+  /// The position of the particle at the beginning of the simulation, in
+  /// logical pixels.
   final double position;
 
   /// The velocity at which the particle is traveling at the beginning of the
-  /// simulation.
+  /// simulation, in logical pixels per second.
   final double velocity;
 
   /// The amount of friction the particle experiences as it travels.
   ///
-  /// The more friction the particle experiences, the sooner it stops.
+  /// The more friction the particle experiences, the sooner it stops and the
+  /// less far it travels.
+  ///
+  /// The default value causes the particle to travel the same total distance
+  /// as in the Android scroll physics.
+  // See mFlingFriction.
   final double friction;
 
+  /// The total time the simulation will run, in seconds.
   late double _duration;
+
+  /// The total, signed, distance the simulation will travel, in logical pixels.
   late double _distance;
 
   // See DECELERATION_RATE.
   static final double _kDecelerationRate = math.log(0.78) / math.log(0.9);
 
-  // See computeDeceleration().
-  static double _decelerationForFriction(double friction) {
-    return friction * 61774.04968;
+  // See INFLEXION.
+  static const double _kInflexion = 0.35;
+
+  // See mPhysicalCoeff.  This has a value of 0.84 times Earth gravity,
+  // expressed in units of logical pixels per second^2.
+  static const double _physicalCoeff =
+      9.80665 // g, in meters per second^2
+        * 39.37 // 1 meter / 1 inch
+        * 160.0 // 1 inch / 1 logical pixel
+        * 0.84; // "look and feel tuning"
+
+  // See getSplineFlingDuration().
+  double _flingDuration() {
+    // See getSplineDeceleration().  That function's value is
+    // math.log(velocity.abs() / referenceVelocity).
+    final double referenceVelocity = friction * _physicalCoeff / _kInflexion;
+
+    // This is the value getSplineFlingDuration() would return, but in seconds.
+    final double androidDuration =
+        math.pow(velocity.abs() / referenceVelocity,
+                 1 / (_kDecelerationRate - 1.0)) as double;
+
+    // We finish a bit sooner than Android, in order to travel the
+    // same total distance.
+    return _kDecelerationRate * _kInflexion * androidDuration;
   }
 
-  // See getSplineFlingDuration(). Returns a value in seconds.
-  double _flingDuration(double velocity) {
-    // See mPhysicalCoeff
-    final double scaledFriction = friction * _decelerationForFriction(0.84);
-
-    // See getSplineDeceleration().
-    final double deceleration = math.log(0.35 * velocity.abs() / scaledFriction);
-
-    return math.exp(deceleration / (_kDecelerationRate - 1.0));
-  }
-
-  // Based on a cubic curve fit to the Scroller.computeScrollOffset() values
-  // produced for an initial velocity of 4000. The value of Scroller.getDuration()
-  // and Scroller.getFinalY() were 686ms and 961 pixels respectively.
-  //
-  // Algebra courtesy of Wolfram Alpha.
-  //
-  // f(x) = scrollOffset, x is time in milliseconds
-  // f(x) = 3.60882×10^-6 x^3 - 0.00668009 x^2 + 4.29427 x - 3.15307
-  // f(x) = 3.60882×10^-6 x^3 - 0.00668009 x^2 + 4.29427 x, so f(0) is 0
-  // f(686ms) = 961 pixels
-  // Scale to f(0 <= t <= 1.0), x = t * 686
-  // f(t) = 1165.03 t^3 - 3143.62 t^2 + 2945.87 t
-  // Scale f(t) so that 0.0 <= f(t) <= 1.0
-  // f(t) = (1165.03 t^3 - 3143.62 t^2 + 2945.87 t) / 961.0
-  //      = 1.2 t^3 - 3.27 t^2 + 3.065 t
-  static const double _initialVelocityPenetration = 3.065;
-  static double _flingDistancePenetration(double t) {
-    return (1.2 * t * t * t) - (3.27 * t * t) + (_initialVelocityPenetration * t);
-  }
-
-  // The derivative of the _flingDistancePenetration() function.
-  static double _flingVelocityPenetration(double t) {
-    return (3.6 * t * t) - (6.54 * t) + _initialVelocityPenetration;
+  // See getSplineFlingDistance().  This returns the same value but with the
+  // sign of [velocity], and in logical pixels.
+  double _flingDistance() {
+    final double distance = velocity * _duration / _kDecelerationRate;
+    assert(() {
+      // This is the more complicated calculation that getSplineFlingDistance()
+      // actually performs, which boils down to the much simpler formula above.
+      final double referenceVelocity = friction * _physicalCoeff / _kInflexion;
+      final double logVelocity = math.log(velocity.abs() / referenceVelocity);
+      final double distanceAgain =
+          friction * _physicalCoeff
+            * math.exp(logVelocity * _kDecelerationRate / (_kDecelerationRate - 1.0));
+      return (distance.abs() - distanceAgain).abs() < tolerance.distance;
+    }());
+    return distance;
   }
 
   @override
   double x(double time) {
     final double t = clampDouble(time / _duration, 0.0, 1.0);
-    return position + _distance * _flingDistancePenetration(t) * velocity.sign;
+    return position + _distance * (1.0 - math.pow(1.0 - t, _kDecelerationRate));
   }
 
   @override
   double dx(double time) {
     final double t = clampDouble(time / _duration, 0.0, 1.0);
-    return _distance * _flingVelocityPenetration(t) * velocity.sign / _duration;
+    return velocity * math.pow(1.0 - t, _kDecelerationRate - 1.0);
   }
 
   @override
diff --git a/framework/lib/src/widgets/scrollable.dart b/framework/lib/src/widgets/scrollable.dart
index 57ff081..4fe3e13 100644
--- a/framework/lib/src/widgets/scrollable.dart
+++ b/framework/lib/src/widgets/scrollable.dart
@@ -359,7 +359,7 @@
   /// If there is no [Scrollable] in the widget tree above the [context], this
   /// method returns false.
   static bool recommendDeferredLoadingForContext(BuildContext context) {
-    final _ScrollableScope? widget = context.getElementForInheritedWidgetOfExactType<_ScrollableScope>()?.widget as _ScrollableScope?;
+    final _ScrollableScope? widget = context.getInheritedWidgetOfExactType<_ScrollableScope>();
     if (widget == null) {
       return false;
     }
@@ -1659,6 +1659,7 @@
   @override
   void assembleSemanticsNode(SemanticsNode node, SemanticsConfiguration config, Iterable<SemanticsNode> children) {
     if (children.isEmpty || !children.first.isTagged(RenderViewport.useTwoPaneSemantics)) {
+      _innerNode = null;
       super.assembleSemanticsNode(node, config, children);
       return;
     }
diff --git a/framework/lib/src/widgets/scrollbar.dart b/framework/lib/src/widgets/scrollbar.dart
index 65ad29c..3f75590 100644
--- a/framework/lib/src/widgets/scrollbar.dart
+++ b/framework/lib/src/widgets/scrollbar.dart
@@ -396,7 +396,7 @@
   Rect? _thumbRect;
   // The current scroll position + _leadingThumbMainAxisOffset
   late double _thumbOffset;
-  // The fraction visible in relation to the trversable length of the track.
+  // The fraction visible in relation to the traversable length of the track.
   late double _thumbExtent;
   // Thumb Offsets
   // The thumb is offset by padding and margins.
@@ -1825,47 +1825,32 @@
       return;
     }
 
-    double scrollIncrement;
-    // Is an increment calculator available?
-    final ScrollIncrementCalculator? calculator = Scrollable.maybeOf(
-      _cachedController!.position.context.notificationContext!,
-    )?.widget.incrementCalculator;
-    if (calculator != null) {
-      scrollIncrement = calculator(
-        ScrollIncrementDetails(
-          type: ScrollIncrementType.page,
-          metrics: _cachedController!.position,
-        ),
-      );
-    } else {
-      // Default page increment
-      scrollIncrement = 0.8 * _cachedController!.position.viewportDimension;
-    }
+    // Determines the scroll direction.
+    final AxisDirection scrollDirection;
 
-    // Adjust scrollIncrement for direction
-    switch (_cachedController!.position.axisDirection) {
+    switch (position.axisDirection) {
       case AxisDirection.up:
-        if (details.localPosition.dy > scrollbarPainter._thumbOffset) {
-          scrollIncrement = -scrollIncrement;
-        }
-        break;
       case AxisDirection.down:
-        if (details.localPosition.dy < scrollbarPainter._thumbOffset) {
-          scrollIncrement = -scrollIncrement;
-        }
-        break;
-      case AxisDirection.right:
-        if (details.localPosition.dx < scrollbarPainter._thumbOffset) {
-          scrollIncrement = -scrollIncrement;
+        if (details.localPosition.dy > scrollbarPainter._thumbOffset) {
+          scrollDirection = AxisDirection.down;
+        } else {
+          scrollDirection = AxisDirection.up;
         }
         break;
       case AxisDirection.left:
+      case AxisDirection.right:
         if (details.localPosition.dx > scrollbarPainter._thumbOffset) {
-          scrollIncrement = -scrollIncrement;
+          scrollDirection = AxisDirection.right;
+        } else {
+          scrollDirection = AxisDirection.left;
         }
-        break;
     }
 
+    final ScrollableState? state = Scrollable.maybeOf(position.context.notificationContext!);
+    final ScrollIntent intent = ScrollIntent(direction: scrollDirection, type: ScrollIncrementType.page);
+    assert(state != null);
+    final double scrollIncrement = ScrollAction.getDirectionalIncrement(state!, intent);
+
     _cachedController!.position.moveTo(
       _cachedController!.position.pixels + scrollIncrement,
       duration: const Duration(milliseconds: 100),
diff --git a/framework/lib/src/widgets/selectable_region.dart b/framework/lib/src/widgets/selectable_region.dart
index fb99008..e6b70e3 100644
--- a/framework/lib/src/widgets/selectable_region.dart
+++ b/framework/lib/src/widgets/selectable_region.dart
@@ -738,10 +738,11 @@
     }
 
     // Web is using native dom elements to enable clipboard functionality of the
-    // toolbar: copy, paste, select, cut. It might also provide additional
-    // functionality depending on the browser (such as translate). Due to this
-    // we should not show a Flutter toolbar for the editable text elements.
-    if (kIsWeb) {
+    // context menu: copy, paste, select, cut. It might also provide additional
+    // functionality depending on the browser (such as translate). Due to this,
+    // we should not show a Flutter toolbar for the editable text elements
+    // unless the browser's context menu is explicitly disabled.
+    if (kIsWeb && BrowserContextMenu.enabled) {
       return false;
     }
 
@@ -973,7 +974,7 @@
   ///
   /// * [SelectableRegion.getSelectableButtonItems], which performs a similar role,
   ///   but for any selectable text, not just specifically SelectableRegion.
-  /// * [EditableTextState.contextMenuButtonItems], which peforms a similar role
+  /// * [EditableTextState.contextMenuButtonItems], which performs a similar role
   ///   but for content that is not just selectable but also editable.
   /// * [contextMenuAnchors], which provides the anchor points for the default
   ///   context menu.
diff --git a/framework/lib/src/widgets/shared_app_data.dart b/framework/lib/src/widgets/shared_app_data.dart
index 518787e..b9e4a69 100644
--- a/framework/lib/src/widgets/shared_app_data.dart
+++ b/framework/lib/src/widgets/shared_app_data.dart
@@ -121,7 +121,7 @@
   /// The type parameter `K` is the type of the value's keyword and `V`
   /// is the type of the value.
   static void setValue<K extends Object, V>(BuildContext context, K key, V value) {
-    final _SharedAppModel? model = context.getElementForInheritedWidgetOfExactType<_SharedAppModel>()?.widget as _SharedAppModel?;
+    final _SharedAppModel? model = context.getInheritedWidgetOfExactType<_SharedAppModel>();
     assert(_debugHasSharedAppData(model, context, 'setValue'));
     model!.sharedAppDataState.setValue<K, V>(key, value);
   }
diff --git a/framework/lib/src/widgets/spell_check.dart b/framework/lib/src/widgets/spell_check.dart
index 63e95b6..20c53e5 100644
--- a/framework/lib/src/widgets/spell_check.dart
+++ b/framework/lib/src/widgets/spell_check.dart
@@ -75,7 +75,7 @@
   spell check enabled   : $_spellCheckEnabled
   spell check service   : $spellCheckService
   misspelled text style : $misspelledTextStyle
-  spell check suggesstions toolbar builder: $spellCheckSuggestionsToolbarBuilder
+  spell check suggestions toolbar builder: $spellCheckSuggestionsToolbarBuilder
 '''
         .trim();
   }
diff --git a/framework/lib/src/widgets/tap_and_drag_gestures.dart b/framework/lib/src/widgets/tap_and_drag_gestures.dart
index 6e99d1d..4009786 100644
--- a/framework/lib/src/widgets/tap_and_drag_gestures.dart
+++ b/framework/lib/src/widgets/tap_and_drag_gestures.dart
@@ -335,7 +335,7 @@
   /// coordinates (the present [globalPosition]) when this callback is triggered.
   ///
   /// When considering a [GestureRecognizer] that tracks the number of consecutive taps,
-  /// this offset is associated with the most recent [PointerDownEvent] that occured.
+  /// this offset is associated with the most recent [PointerDownEvent] that occurred.
   final Offset offsetFromOrigin;
 
   /// A local delta offset from the point where the drag initially contacted
@@ -343,7 +343,7 @@
   /// coordinates (the present [localPosition]) when this callback is triggered.
   ///
   /// When considering a [GestureRecognizer] that tracks the number of consecutive taps,
-  /// this offset is associated with the most recent [PointerDownEvent] that occured.
+  /// this offset is associated with the most recent [PointerDownEvent] that occurred.
   final Offset localOffsetFromOrigin;
 
   /// If this tap is in a series of taps, then this value represents
@@ -678,7 +678,7 @@
 /// ### When competing with `TapGestureRecognizer` and `DragGestureRecognizer`
 ///
 /// Similar to [TapGestureRecognizer] and [DragGestureRecognizer],
-/// [TapAndDragGestureRecognizer] will not aggresively declare victory when it detects
+/// [TapAndDragGestureRecognizer] will not aggressively declare victory when it detects
 /// a tap, so when it is competing with those gesture recognizers and others it has a chance
 /// of losing.
 ///
diff --git a/framework/lib/src/widgets/text_selection.dart b/framework/lib/src/widgets/text_selection.dart
index 72d971b..566c405 100644
--- a/framework/lib/src/widgets/text_selection.dart
+++ b/framework/lib/src/widgets/text_selection.dart
@@ -718,7 +718,7 @@
       ));
 
       final TextSelection currentSelection = TextSelection.fromPosition(position);
-      _handleSelectionHandleChanged(currentSelection, isEnd: true);
+      _handleSelectionHandleChanged(currentSelection);
       return;
     }
 
@@ -749,7 +749,7 @@
         break;
     }
 
-    _handleSelectionHandleChanged(newSelection, isEnd: true);
+    _handleSelectionHandleChanged(newSelection);
 
      _selectionOverlay.updateMagnifier(_buildMagnifier(
       currentTextPosition: newSelection.extent,
@@ -814,7 +814,7 @@
       ));
 
       final TextSelection currentSelection = TextSelection.fromPosition(position);
-      _handleSelectionHandleChanged(currentSelection, isEnd: false);
+      _handleSelectionHandleChanged(currentSelection);
       return;
     }
 
@@ -851,7 +851,7 @@
       renderEditable: renderObject,
     ));
 
-    _handleSelectionHandleChanged(newSelection, isEnd: false);
+    _handleSelectionHandleChanged(newSelection);
   }
 
   void _handleAnyDragEnd(DragEndDetails details) {
@@ -874,13 +874,11 @@
     }
   }
 
-  void _handleSelectionHandleChanged(TextSelection newSelection, {required bool isEnd}) {
-    final TextPosition textPosition = isEnd ? newSelection.extent : newSelection.base;
+  void _handleSelectionHandleChanged(TextSelection newSelection) {
     selectionDelegate.userUpdateTextEditingValue(
       _value.copyWith(selection: newSelection),
       SelectionChangedCause.drag,
     );
-    selectionDelegate.bringIntoView(textPosition);
   }
 
   TextSelectionHandleType _chooseType(
@@ -983,13 +981,13 @@
   /// since magnifiers may hide themselves. If this info is needed, check
   /// [MagnifierController.shown].
   /// {@endtemplate}
-  void showMagnifier(MagnifierInfo initalMagnifierInfo) {
+  void showMagnifier(MagnifierInfo initialMagnifierInfo) {
     if (_toolbar != null || _contextMenuControllerIsShown) {
       hideToolbar();
     }
 
-    // Start from empty, so we don't utilize any rememnant values.
-    _magnifierInfo.value = initalMagnifierInfo;
+    // Start from empty, so we don't utilize any remnant values.
+    _magnifierInfo.value = initialMagnifierInfo;
 
     // Pre-build the magnifiers so we can tell if we've built something
     // or not. If we don't build a magnifiers, then we should not
@@ -2430,7 +2428,7 @@
   @protected
   void onDoubleTapDown(TapDragDownDetails details) {
     if (delegate.selectionEnabled) {
-      renderEditable.selectWord(cause: SelectionChangedCause.tap);
+      renderEditable.selectWord(cause: SelectionChangedCause.doubleTap);
       if (shouldShowSelectionToolbar) {
         editableText.showToolbar();
       }
@@ -3076,7 +3074,7 @@
   /// waiting to receive the clipboard contents for the first time.
   unknown,
 
-  /// The content on the clipboard is not pastable, such as when it is empty.
+  /// The content on the clipboard is not pasteable, such as when it is empty.
   notPasteable,
 }
 
diff --git a/framework/lib/src/widgets/ticker_provider.dart b/framework/lib/src/widgets/ticker_provider.dart
index 7f87844..31a32a3 100644
--- a/framework/lib/src/widgets/ticker_provider.dart
+++ b/framework/lib/src/widgets/ticker_provider.dart
@@ -95,7 +95,7 @@
   /// In the absence of a [TickerMode] widget, this function returns a
   /// [ValueNotifier], whose [ValueNotifier.value] is always true.
   static ValueNotifier<bool> getNotifier(BuildContext context) {
-    final _EffectiveTickerMode? widget = context.getElementForInheritedWidgetOfExactType<_EffectiveTickerMode>()?.widget as _EffectiveTickerMode?;
+    final _EffectiveTickerMode? widget = context.getInheritedWidgetOfExactType<_EffectiveTickerMode>();
     return widget?.notifier ?? ValueNotifier<bool>(true);
   }
 
diff --git a/framework/lib/src/widgets/widget_inspector.dart b/framework/lib/src/widgets/widget_inspector.dart
index f8d557f..2c7e708 100644
--- a/framework/lib/src/widgets/widget_inspector.dart
+++ b/framework/lib/src/widgets/widget_inspector.dart
@@ -932,7 +932,7 @@
         groupName: _consoleObjectGroup,
         subtreeDepth: 5,
         includeProperties: true,
-        maxDescendentsTruncatableNode: 5,
+        maxDescendantsTruncatableNode: 5,
         service: this,
       ),
     )!;
@@ -3566,7 +3566,7 @@
   InspectorSerializationDelegate({
     this.groupName,
     this.summaryTree = false,
-    this.maxDescendentsTruncatableNode = -1,
+    this.maxDescendantsTruncatableNode = -1,
     this.expandPropertyValues = true,
     this.subtreeDepth = 1,
     this.includeProperties = false,
@@ -3587,8 +3587,8 @@
   /// Whether the tree should only include nodes created by the local project.
   final bool summaryTree;
 
-  /// Maximum descendents of [DiagnosticsNode] before truncating.
-  final int maxDescendentsTruncatableNode;
+  /// Maximum descendants of [DiagnosticsNode] before truncating.
+  final int maxDescendantsTruncatableNode;
 
   @override
   final bool includeProperties;
@@ -3663,10 +3663,10 @@
 
   @override
   List<DiagnosticsNode> truncateNodesList(List<DiagnosticsNode> nodes, DiagnosticsNode? owner) {
-    if (maxDescendentsTruncatableNode >= 0 &&
+    if (maxDescendantsTruncatableNode >= 0 &&
         owner!.allowTruncate == true &&
-        nodes.length > maxDescendentsTruncatableNode) {
-      nodes = service._truncateNodes(nodes, maxDescendentsTruncatableNode);
+        nodes.length > maxDescendantsTruncatableNode) {
+      nodes = service._truncateNodes(nodes, maxDescendantsTruncatableNode);
     }
     return nodes;
   }
@@ -3676,7 +3676,7 @@
     return InspectorSerializationDelegate(
       groupName: groupName,
       summaryTree: summaryTree,
-      maxDescendentsTruncatableNode: maxDescendentsTruncatableNode,
+      maxDescendantsTruncatableNode: maxDescendantsTruncatableNode,
       expandPropertyValues: expandPropertyValues ?? this.expandPropertyValues,
       subtreeDepth: subtreeDepth ?? this.subtreeDepth,
       includeProperties: includeProperties ?? this.includeProperties,
@@ -3711,8 +3711,8 @@
 /// factory. The framework will then instrument that function in the same way
 /// as it does for [Widget] constructors.
 ///
-/// Note that the function **must not** have optional positional parameters for
-/// tracking to work correctly.
+/// Tracking will not work correctly if the function has optional positional
+/// parameters.
 ///
 /// Currently this annotation is only supported on extension methods.
 ///