diff --git a/packages/flutter/lib/src/scheduler/ticker.dart b/packages/flutter/lib/src/scheduler/ticker.dart index b088dab795b..b81eea9e9fe 100644 --- a/packages/flutter/lib/src/scheduler/ticker.dart +++ b/packages/flutter/lib/src/scheduler/ticker.dart @@ -91,6 +91,14 @@ class Ticker { TickerFuture? _future; + /// If true, this ticker will request frames using + /// [SchedulerBinding.scheduleForcedFrame] instead of [SchedulerBinding.scheduleFrame]. + /// + /// This allows granular control to advance frames even when frames are + /// typically disabled (e.g. when the app is in the background). This should be + /// used sparingly as it can increase battery usage. + bool forceFrames = false; + /// Whether this ticker has been silenced. /// /// While silenced, a ticker's clock can still run, but the callback will not @@ -282,9 +290,15 @@ class Ticker { void scheduleTick({bool rescheduling = false}) { assert(!scheduled); assert(shouldScheduleTick); + if (forceFrames) { + SchedulerBinding.instance.scheduleForcedFrame(); + } else { + SchedulerBinding.instance.scheduleFrame(); + } _animationId = SchedulerBinding.instance.scheduleFrameCallback( _tick, rescheduling: rescheduling, + scheduleNewFrame: false, ); } diff --git a/packages/flutter/lib/src/widgets/ticker_provider.dart b/packages/flutter/lib/src/widgets/ticker_provider.dart index cb3c1c8c55b..247933881ec 100644 --- a/packages/flutter/lib/src/widgets/ticker_provider.dart +++ b/packages/flutter/lib/src/widgets/ticker_provider.dart @@ -8,6 +8,7 @@ library; import 'package:flutter/foundation.dart'; import 'package:flutter/scheduler.dart'; +import 'basic.dart'; import 'framework.dart'; export 'package:flutter/scheduler.dart' show TickerProvider; @@ -22,8 +23,13 @@ export 'package:flutter/scheduler.dart' show TickerProvider; /// widget-aware ticker providers. For example, using a /// [TickerProviderStateMixin] or a [SingleTickerProviderStateMixin]. class TickerMode extends StatefulWidget { - /// Creates a widget that enables or disables tickers. - const TickerMode({super.key, required this.enabled, required this.child}); + /// Creates a widget that enables or disables tickers and optionally forces frames. + const TickerMode({ + super.key, + required this.enabled, + required this.child, + this.forceFrames = false, + }); /// The requested ticker mode for this subtree. /// @@ -38,6 +44,14 @@ class TickerMode extends StatefulWidget { /// they just don't call their callbacks. Time still elapses. final bool enabled; + /// If true, tickers in this subtree will force frames even if + /// frames would normally not be scheduled (e.g. even if the + /// device's screen is turned off). + /// + /// Use sparingly as this will cause significantly higher battery + /// usage when the device should be idle. + final bool forceFrames; + /// The widget below this widget in the tree. /// /// {@macro flutter.widgets.ProxyWidget.child} @@ -56,6 +70,10 @@ class TickerMode extends StatefulWidget { /// ```dart /// bool tickingEnabled = TickerMode.of(context); /// ``` + @Deprecated( + 'Use TickerMode.valuesOf to get both enabled and forceFrames. ' + 'This feature was deprecated after v3.35.0-0.0.pre.', + ) static bool of(BuildContext context) { final _EffectiveTickerMode? widget = context .dependOnInheritedWidgetOfExactType<_EffectiveTickerMode>(); @@ -65,7 +83,7 @@ class TickerMode extends StatefulWidget { /// Obtains a [ValueListenable] from the [TickerMode] surrounding the `context`, /// which indicates whether tickers are enabled in the given subtree. /// - /// When that [TickerMode] enabled or disabled tickers, the listenable notifies + /// When that [TickerMode] enables or disables tickers, the listenable notifies /// its listeners. /// /// While the [ValueListenable] is stable for the lifetime of the surrounding @@ -87,29 +105,119 @@ class TickerMode extends StatefulWidget { /// dependency between the provided `context` and the ancestor [TickerMode]. /// In this case, the widget automatically rebuilds when the ticker mode /// changes or when it is moved to a new [TickerMode] ancestor, which - /// simplifies the management cost in the widget at the expensive of some + /// simplifies the management cost in the widget at the expense of some /// potential unnecessary rebuilds. /// /// In the absence of a [TickerMode] widget, this function returns a /// [ValueListenable], whose [ValueListenable.value] is always true. + @Deprecated( + 'Use TickerMode.getValuesNotifier to get both enabled and forceFrames. ' + 'This feature was deprecated after v3.35.0-0.0.pre.', + ) static ValueListenable getNotifier(BuildContext context) { final _EffectiveTickerMode? widget = context .getInheritedWidgetOfExactType<_EffectiveTickerMode>(); return widget?.notifier ?? const _ConstantValueListenable(true); } + /// Returns the requested ticker mode values for this subtree and establishes + /// a dependency on the ancestor [TickerMode], if any. + /// + /// This is used automatically by [TickerProviderStateMixin] and + /// [SingleTickerProviderStateMixin] to decide if their tickers should be + /// enabled or disabled. + /// + /// In the absence of a [TickerMode] widget, this defaults to enabled + /// tickers that don't force frames. + /// + /// Typical usage is as follows: + /// + /// ```dart + /// bool tickingEnabled = TickerMode.valuesOf(context).enabled; + /// ``` + static TickerModeData valuesOf(BuildContext context) { + final _EffectiveTickerMode? widget = context + .dependOnInheritedWidgetOfExactType<_EffectiveTickerMode>(); + return widget?.values ?? TickerModeData.fallback; + } + + /// Obtains a [ValueListenable] from the [TickerMode] surrounding the `context`, + /// which indicates whether tickers are enabled in the given subtree. + /// + /// When that [TickerMode] enabled or disabled tickers, the listenable notifies + /// its listeners. + /// + /// While the [ValueListenable] is stable for the lifetime of the surrounding + /// [TickerMode], calling this method does not establish a dependency between + /// the `context` and the [TickerMode] and the widget owning the `context` + /// does not rebuild when the ticker mode data changes. This is preferable + /// when the ticker mode does not impact what is currently rendered on screen, + /// e.g. because it is only used to mute/unmute a [Ticker]. Since no dependency + /// is established, the widget owning the `context` is also not informed when + /// it is moved to a new location in the tree where it may have a different + /// [TickerMode] ancestor. When this happens, the widget must manually + /// unsubscribe from the old listenable, obtain a new one from the new ancestor + /// [TickerMode] by calling this method again, and re-subscribe to it. + /// [StatefulWidget]s can, for example, do this in [State.activate], + /// which is called after the widget has been moved to a new location. + /// + /// Alternatively, [of] can be used instead of this method to create a + /// dependency between the provided `context` and the ancestor [TickerMode]. + /// In this case, the widget automatically rebuilds when the ticker mode + /// changes or when it is moved to a new [TickerMode] ancestor, which + /// simplifies the management cost in the widget at the expensive of some + /// potential unnecessary rebuilds. + /// + /// In the absence of a [TickerMode] widget, this function returns a + /// [ValueListenable], whose [ValueListenable.value] is + /// [TickerModeData.fallback]. + static ValueListenable getValuesNotifier(BuildContext context) { + final _EffectiveTickerMode? widget = context + .getInheritedWidgetOfExactType<_EffectiveTickerMode>(); + return widget?.valuesNotifier ?? + const _ConstantTickerModeDataListenable(TickerModeData.fallback); + } + + /// Creates a [TickerMode] that overrides the ambient ticker mode values. + /// + /// The given `enabled` and `forceFrames` override the ambient values when not null; + /// otherwise the ambient values are preserved. + static Widget merge({Key? key, bool? enabled, bool? forceFrames, required Widget child}) { + return Builder( + builder: (BuildContext context) { + final _EffectiveTickerMode? parent = context + .dependOnInheritedWidgetOfExactType<_EffectiveTickerMode>(); + final bool parentEnabled = parent?.enabled ?? TickerModeData.fallback.enabled; + final bool parentForce = parent?.forceFrames ?? TickerModeData.fallback.forceFrames; + return TickerMode( + key: key, + enabled: enabled ?? parentEnabled, + forceFrames: forceFrames ?? parentForce, + child: child, + ); + }, + ); + } + @override State createState() => _TickerModeState(); } class _TickerModeState extends State { - bool _ancestorTicketMode = true; - final ValueNotifier _effectiveMode = ValueNotifier(true); + bool _ancestorTickerMode = TickerModeData.fallback.enabled; + bool _ancestorForceFrames = TickerModeData.fallback.forceFrames; + final ValueNotifier _effectiveMode = ValueNotifier(TickerModeData.fallback.enabled); + final ValueNotifier _effectiveValues = ValueNotifier( + TickerModeData.fallback, + ); @override void didChangeDependencies() { super.didChangeDependencies(); - _ancestorTicketMode = TickerMode.of(context); + final _EffectiveTickerMode? parent = context + .dependOnInheritedWidgetOfExactType<_EffectiveTickerMode>(); + _ancestorTickerMode = parent?.enabled ?? TickerModeData.fallback.enabled; + _ancestorForceFrames = parent?.forceFrames ?? TickerModeData.fallback.forceFrames; _updateEffectiveMode(); } @@ -122,18 +230,24 @@ class _TickerModeState extends State { @override void dispose() { _effectiveMode.dispose(); + _effectiveValues.dispose(); super.dispose(); } void _updateEffectiveMode() { - _effectiveMode.value = _ancestorTicketMode && widget.enabled; + final bool enabled = _ancestorTickerMode && widget.enabled; + final bool force = _ancestorForceFrames || widget.forceFrames; + _effectiveMode.value = enabled; + _effectiveValues.value = TickerModeData(enabled: enabled, forceFrames: force); } @override Widget build(BuildContext context) { return _EffectiveTickerMode( enabled: _effectiveMode.value, + forceFrames: _effectiveValues.value.forceFrames, notifier: _effectiveMode, + valuesNotifier: _effectiveValues, child: widget.child, ); } @@ -154,13 +268,24 @@ class _TickerModeState extends State { } class _EffectiveTickerMode extends InheritedWidget { - const _EffectiveTickerMode({required this.enabled, required this.notifier, required super.child}); + const _EffectiveTickerMode({ + required this.enabled, + required this.forceFrames, + required this.notifier, + required this.valuesNotifier, + required super.child, + }); final bool enabled; + final bool forceFrames; final ValueNotifier notifier; + final ValueNotifier valuesNotifier; + + TickerModeData get values => valuesNotifier.value; @override - bool updateShouldNotify(_EffectiveTickerMode oldWidget) => enabled != oldWidget.enabled; + bool updateShouldNotify(_EffectiveTickerMode oldWidget) => + enabled != oldWidget.enabled || forceFrames != oldWidget.forceFrames; @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { @@ -247,7 +372,7 @@ mixin SingleTickerProviderStateMixin on State super.dispose(); } - ValueListenable? _tickerModeNotifier; + ValueListenable? _tickerModeNotifier; @override void activate() { @@ -257,10 +382,16 @@ mixin SingleTickerProviderStateMixin on State _updateTicker(); } - void _updateTicker() => _ticker?.muted = !_tickerModeNotifier!.value; + void _updateTicker() { + final TickerModeData values = _tickerModeNotifier!.value; + if (_ticker != null) { + _ticker!.muted = !values.enabled; + _ticker!.forceFrames = values.forceFrames; + } + } void _updateTickerModeNotifier() { - final ValueListenable newNotifier = TickerMode.getNotifier(context); + final ValueListenable newNotifier = TickerMode.getValuesNotifier(context); if (newNotifier == _tickerModeNotifier) { return; } @@ -318,11 +449,15 @@ mixin TickerProviderStateMixin on State implements } assert(_tickerModeNotifier != null); _tickers ??= <_WidgetTicker>{}; - final _WidgetTicker result = _WidgetTicker( - onTick, - this, - debugLabel: kDebugMode ? 'created by ${describeIdentity(this)}' : null, - )..muted = !_tickerModeNotifier!.value; + final TickerModeData values = _tickerModeNotifier!.value; + final _WidgetTicker result = + _WidgetTicker( + onTick, + this, + debugLabel: kDebugMode ? 'created by ${describeIdentity(this)}' : null, + ) + ..muted = !values.enabled + ..forceFrames = values.forceFrames; _tickers!.add(result); return result; } @@ -333,7 +468,7 @@ mixin TickerProviderStateMixin on State implements _tickers!.remove(ticker); } - ValueListenable? _tickerModeNotifier; + ValueListenable? _tickerModeNotifier; @override void activate() { @@ -345,15 +480,17 @@ mixin TickerProviderStateMixin on State implements void _updateTickers() { if (_tickers != null) { - final bool muted = !_tickerModeNotifier!.value; + final TickerModeData values = _tickerModeNotifier!.value; + final bool muted = !values.enabled; for (final Ticker ticker in _tickers!) { ticker.muted = muted; + ticker.forceFrames = values.forceFrames; } } } void _updateTickerModeNotifier() { - final ValueListenable newNotifier = TickerMode.getNotifier(context); + final ValueListenable newNotifier = TickerMode.getValuesNotifier(context); if (newNotifier == _tickerModeNotifier) { return; } @@ -442,3 +579,76 @@ class _ConstantValueListenable implements ValueListenable { @override final T value; } + +/// Immutable compound values that describe the effective ticker behavior +/// for a subtree. +/// +/// Instances of this class are produced by [TickerMode.valuesOf] and +/// [TickerMode.getValuesNotifier] and reflect the values that apply at a given +/// location in the widget tree after taking ancestor [TickerMode] widgets into +/// account. +/// +/// Semantics of the fields: +/// - [enabled]: A ticker is considered enabled only if all ancestor +/// [TickerMode.enabled] values and the local [TickerMode.enabled] are true +/// (logical AND). When false, tickers are muted (time still elapses but +/// callbacks are not invoked). +/// - [forceFrames]: When true, tickers in the subtree request frames using +/// [SchedulerBinding.scheduleForcedFrame] while active. This value is +/// combined across ancestors using logical OR, so any ancestor requesting +/// forced frames enables it for the subtree. +/// +/// For most widgets, reading these values is unnecessary; mixins such as +/// [SingleTickerProviderStateMixin] and [TickerProviderStateMixin] apply them +/// automatically to the [Ticker]s they vend. Use this class when you need to +/// observe or react to ticker policy explicitly. +@immutable +class TickerModeData { + /// Creates a [TickerModeData]. + const TickerModeData({required this.enabled, required this.forceFrames}); + + /// Fallback values used when there is no ancestor [TickerMode]. + /// + /// This corresponds to tickers being enabled and not forcing frames. + static const TickerModeData fallback = TickerModeData(enabled: true, forceFrames: false); + + /// Whether tickers are enabled (not muted) for the subtree. + /// + /// Effective value is the logical AND of all ancestor and local + /// [TickerMode.enabled] values. + final bool enabled; + + /// Whether tickers should request forced frames while active. + /// + /// Effective value is the logical OR of all ancestor and local + /// [TickerMode.forceFrames] values. Forcing frames may increase battery + /// usage, so use sparingly. + final bool forceFrames; + + @override + bool operator ==(Object other) { + if (identical(other, this)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + return other is TickerModeData && other.enabled == enabled && other.forceFrames == forceFrames; + } + + @override + int get hashCode => Object.hash(enabled, forceFrames); +} + +class _ConstantTickerModeDataListenable implements ValueListenable { + const _ConstantTickerModeDataListenable(this.value); + + @override + void addListener(VoidCallback listener) {} + + @override + void removeListener(VoidCallback listener) {} + + @override + final TickerModeData value; +} diff --git a/packages/flutter/test/rendering/rendering_tester.dart b/packages/flutter/test/rendering/rendering_tester.dart index ed95de0a56c..5c35c7085fd 100644 --- a/packages/flutter/test/rendering/rendering_tester.dart +++ b/packages/flutter/test/rendering/rendering_tester.dart @@ -348,6 +348,9 @@ class FakeTicker implements Ticker { @override bool muted = false; + @override + bool forceFrames = false; + @override void absorbTicker(Ticker originalTicker) {} diff --git a/packages/flutter/test/scheduler/binding_test.dart b/packages/flutter/test/scheduler/binding_test.dart index 5079db9266f..56ff0faa705 100644 --- a/packages/flutter/test/scheduler/binding_test.dart +++ b/packages/flutter/test/scheduler/binding_test.dart @@ -9,12 +9,24 @@ import 'package:flutter_test/flutter_test.dart'; void main() { WidgetsFlutterBinding.ensureInitialized(); - test('scheduleForcedFrame sets up frame callbacks', () async { + test('scheduleForcedFrame sets up frame callbacks', () { SchedulerBinding.instance.scheduleForcedFrame(); expect(SchedulerBinding.instance.platformDispatcher.onBeginFrame, isNotNull); }); - test('debugAssertNoTimeDilation does not throw if time dilate already reset', () async { + test('Ticker.forceFrames requests forced frames', () async { + final Ticker t = Ticker((_) {}); + t.forceFrames = true; + final TickerFuture f = t.start(); + addTearDown(() async { + t.stop(); + await f; + }); + // A forced frame should be scheduled even if frames are otherwise disabled. + expect(SchedulerBinding.instance.hasScheduledFrame, isTrue); + }); + + test('debugAssertNoTimeDilation does not throw if time dilate already reset', () { timeDilation = 2.0; timeDilation = 1.0; SchedulerBinding.instance.debugAssertNoTimeDilation('reason'); // no error diff --git a/packages/flutter/test/widgets/ticker_mode_test.dart b/packages/flutter/test/widgets/ticker_mode_test.dart index 24432d6717f..9d6bf1af20b 100644 --- a/packages/flutter/test/widgets/ticker_mode_test.dart +++ b/packages/flutter/test/widgets/ticker_mode_test.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -216,6 +217,302 @@ void main() { expect(tickingState().ticker.isTicking, isFalse); expect(tickingState().buildCount, 1); }); + + testWidgets('TickerMode.forceFrames propagates to SingleTickerProviderStateMixin', ( + WidgetTester tester, + ) async { + Widget widgetUnderTest({required bool forceFrames}) { + return TickerMode(enabled: true, forceFrames: forceFrames, child: const _TickingWidget()); + } + + _TickingWidgetState state() => tester.state<_TickingWidgetState>(find.byType(_TickingWidget)); + + await tester.pumpWidget(widgetUnderTest(forceFrames: false)); + expect(state().ticker.forceFrames, isFalse); + expect(state().buildCount, 1); + + await tester.pumpWidget(widgetUnderTest(forceFrames: true)); + expect(state().ticker.forceFrames, isTrue); + expect(state().buildCount, 1); + + await tester.pumpWidget(widgetUnderTest(forceFrames: false)); + expect(state().ticker.forceFrames, isFalse); + expect(state().buildCount, 1); + }); + + testWidgets('TickerMode.forceFrames propagates to TickerProviderStateMixin', ( + WidgetTester tester, + ) async { + Widget widgetUnderTest({required bool forceFrames}) { + return TickerMode( + enabled: true, + forceFrames: forceFrames, + child: const _MultiTickingWidget(), + ); + } + + _MultiTickingWidgetState state() => + tester.state<_MultiTickingWidgetState>(find.byType(_MultiTickingWidget)); + + await tester.pumpWidget(widgetUnderTest(forceFrames: false)); + expect(state().ticker.forceFrames, isFalse); + expect(state().buildCount, 1); + + await tester.pumpWidget(widgetUnderTest(forceFrames: true)); + expect(state().ticker.forceFrames, isTrue); + expect(state().buildCount, 1); + + await tester.pumpWidget(widgetUnderTest(forceFrames: false)); + expect(state().ticker.forceFrames, isFalse); + expect(state().buildCount, 1); + }); + + testWidgets('Nested TickerMode.forceFrames uses OR semantics', (WidgetTester tester) async { + Widget nestedTickerModes({required bool innerForce, required bool outerForce}) { + return Directionality( + textDirection: TextDirection.rtl, + child: TickerMode( + enabled: true, + forceFrames: outerForce, + child: Row( + children: [ + const _TickingWidget(key: ValueKey('outer')), + TickerMode( + enabled: true, + forceFrames: innerForce, + child: const _TickingWidget(key: ValueKey('inner')), + ), + ], + ), + ), + ); + } + + _TickingWidgetState outerState() => + tester.state<_TickingWidgetState>(find.byKey(const ValueKey('outer'))); + _TickingWidgetState innerState() => + tester.state<_TickingWidgetState>(find.byKey(const ValueKey('inner'))); + + // Both false -> both should not force frames + await tester.pumpWidget(nestedTickerModes(outerForce: false, innerForce: false)); + expect(outerState().ticker.forceFrames, isFalse); + expect(innerState().ticker.forceFrames, isFalse); + + // Outer true -> both should force frames (OR semantics) + await tester.pumpWidget(nestedTickerModes(outerForce: true, innerForce: false)); + expect(outerState().ticker.forceFrames, isTrue); + expect(innerState().ticker.forceFrames, isTrue); + + // Inner true -> only inner should force frames + await tester.pumpWidget(nestedTickerModes(outerForce: false, innerForce: true)); + expect(outerState().ticker.forceFrames, isFalse); + expect(innerState().ticker.forceFrames, isTrue); + + // Both true -> both should force frames + await tester.pumpWidget(nestedTickerModes(outerForce: true, innerForce: true)); + expect(outerState().ticker.forceFrames, isTrue); + expect(innerState().ticker.forceFrames, isTrue); + }); + + testWidgets('TickerMode.merge preserves ambient enabled and overrides forceFrames', ( + WidgetTester tester, + ) async { + await tester.pumpWidget( + TickerMode( + enabled: false, + child: TickerMode.merge(forceFrames: true, child: const _TickingWidget()), + ), + ); + + final _TickingWidgetState state = tester.state<_TickingWidgetState>( + find.byType(_TickingWidget), + ); + // enabled should be false (inherited from ancestor) + expect(state.ticker.muted, isTrue); + // forceFrames should be true (merged override) + expect(state.ticker.forceFrames, isTrue); + }); + + testWidgets('TickerMode.merge respects AND semantics for enabled', (WidgetTester tester) async { + // Test that merge cannot override parent's enabled=false due to AND semantics + await tester.pumpWidget( + TickerMode( + enabled: false, + child: TickerMode.merge(enabled: true, child: const _TickingWidget()), + ), + ); + + final _TickingWidgetState state = tester.state<_TickingWidgetState>( + find.byType(_TickingWidget), + ); + // enabled uses AND semantics - child cannot re-enable when parent disables + expect(state.ticker.muted, isTrue); + // forceFrames should be false (inherited) + expect(state.ticker.forceFrames, isFalse); + }); + + testWidgets('TickerMode.merge can disable when parent is enabled', (WidgetTester tester) async { + // Test that merge can set enabled=false when parent is enabled=true + await tester.pumpWidget( + TickerMode( + enabled: true, + child: TickerMode.merge(enabled: false, child: const _TickingWidget()), + ), + ); + + final _TickingWidgetState state = tester.state<_TickingWidgetState>( + find.byType(_TickingWidget), + ); + // enabled=false overrides parent's enabled=true (AND: true && false = false) + expect(state.ticker.muted, isTrue); + // forceFrames should be false (inherited) + expect(state.ticker.forceFrames, isFalse); + }); + + testWidgets('TickerMode.merge with no ancestor uses fallback values', ( + WidgetTester tester, + ) async { + await tester.pumpWidget(TickerMode.merge(forceFrames: true, child: const _TickingWidget())); + + final _TickingWidgetState state = tester.state<_TickingWidgetState>( + find.byType(_TickingWidget), + ); + // enabled should be true (fallback) + expect(state.ticker.muted, isFalse); + // forceFrames should be true (merged override) + expect(state.ticker.forceFrames, isTrue); + }); + + testWidgets('TickerMode.valuesOf returns correct values', (WidgetTester tester) async { + late TickerModeData capturedValues; + + await tester.pumpWidget( + TickerMode( + enabled: false, + forceFrames: true, + child: Builder( + builder: (BuildContext context) { + capturedValues = TickerMode.valuesOf(context); + return Container(); + }, + ), + ), + ); + + expect(capturedValues.enabled, isFalse); + expect(capturedValues.forceFrames, isTrue); + }); + + testWidgets('TickerMode.valuesOf returns fallback when no ancestor', (WidgetTester tester) async { + late TickerModeData capturedValues; + + await tester.pumpWidget( + Builder( + builder: (BuildContext context) { + capturedValues = TickerMode.valuesOf(context); + return Container(); + }, + ), + ); + + expect(capturedValues.enabled, isTrue); + expect(capturedValues.forceFrames, isFalse); + expect(capturedValues, equals(TickerModeData.fallback)); + }); + + testWidgets('TickerMode.getValuesNotifier notifies listeners', (WidgetTester tester) async { + late ValueListenable notifier; + final List notifiedValues = []; + + await tester.pumpWidget( + TickerMode( + enabled: true, + child: Builder( + builder: (BuildContext context) { + notifier = TickerMode.getValuesNotifier(context); + return Container(); + }, + ), + ), + ); + + notifier.addListener(() { + notifiedValues.add(notifier.value); + }); + + expect(notifier.value.enabled, isTrue); + expect(notifier.value.forceFrames, isFalse); + + // Change forceFrames + await tester.pumpWidget( + TickerMode( + enabled: true, + forceFrames: true, + child: Builder( + builder: (BuildContext context) { + return Container(); + }, + ), + ), + ); + + expect(notifiedValues.length, 1); + expect(notifiedValues.last.enabled, isTrue); + expect(notifiedValues.last.forceFrames, isTrue); + }); + + test('TickerModeData equality works correctly', () { + const TickerModeData data1 = TickerModeData.fallback; + const TickerModeData data2 = TickerModeData.fallback; + const TickerModeData data3 = TickerModeData(enabled: false, forceFrames: false); + const TickerModeData data4 = TickerModeData(enabled: true, forceFrames: true); + + expect(data1, equals(data2)); + expect(data1, isNot(equals(data3))); + expect(data1, isNot(equals(data4))); + expect(data1.hashCode, equals(data2.hashCode)); + expect(data1, equals(TickerModeData.fallback)); + }); + + testWidgets('Deprecated TickerMode.of still works', (WidgetTester tester) async { + late bool capturedEnabled; + + await tester.pumpWidget( + TickerMode( + enabled: false, + forceFrames: true, + child: Builder( + builder: (BuildContext context) { + // ignore: deprecated_member_use + capturedEnabled = TickerMode.of(context); + return Container(); + }, + ), + ), + ); + + expect(capturedEnabled, isFalse); + }); + + testWidgets('Deprecated TickerMode.getNotifier still works', (WidgetTester tester) async { + late ValueListenable notifier; + + await tester.pumpWidget( + TickerMode( + enabled: false, + forceFrames: true, + child: Builder( + builder: (BuildContext context) { + // ignore: deprecated_member_use + notifier = TickerMode.getNotifier(context); + return Container(); + }, + ), + ), + ); + + expect(notifier.value, isFalse); + }); } class _TickingWidget extends StatefulWidget {