Unverified Commit fe8e882a authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

Do not rebuild when TickerMode changes (#93166)

parent f129fb0a
...@@ -1524,10 +1524,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1524,10 +1524,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
ScrollController? _internalScrollController; ScrollController? _internalScrollController;
ScrollController get _scrollController => widget.scrollController ?? (_internalScrollController ??= ScrollController()); ScrollController get _scrollController => widget.scrollController ?? (_internalScrollController ??= ScrollController());
late final AnimationController _cursorBlinkOpacityController = AnimationController( AnimationController? _cursorBlinkOpacityController;
vsync: this,
duration: _fadeDuration,
)..addListener(_onCursorColorTick);
final LayerLink _toolbarLayerLink = LayerLink(); final LayerLink _toolbarLayerLink = LayerLink();
final LayerLink _startHandleLayerLink = LayerLink(); final LayerLink _startHandleLayerLink = LayerLink();
...@@ -1564,14 +1561,12 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1564,14 +1561,12 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
// cursor position after the user has finished placing it. // cursor position after the user has finished placing it.
static const Duration _floatingCursorResetTime = Duration(milliseconds: 125); static const Duration _floatingCursorResetTime = Duration(milliseconds: 125);
late final AnimationController _floatingCursorResetController = AnimationController( AnimationController? _floatingCursorResetController;
vsync: this,
)..addListener(_onFloatingCursorResetTick);
@override @override
bool get wantKeepAlive => widget.focusNode.hasFocus; bool get wantKeepAlive => widget.focusNode.hasFocus;
Color get _cursorColor => widget.cursorColor.withOpacity(_cursorBlinkOpacityController.value); Color get _cursorColor => widget.cursorColor.withOpacity(_cursorBlinkOpacityController!.value);
@override @override
bool get cutEnabled => widget.toolbarOptions.cut && !widget.readOnly; bool get cutEnabled => widget.toolbarOptions.cut && !widget.readOnly;
...@@ -1698,6 +1693,10 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1698,6 +1693,10 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_cursorBlinkOpacityController = AnimationController(
vsync: this,
duration: _fadeDuration,
)..addListener(_onCursorColorTick);
_clipboardStatus?.addListener(_onChangedClipboardStatus); _clipboardStatus?.addListener(_onChangedClipboardStatus);
widget.controller.addListener(_didChangeTextEditingValue); widget.controller.addListener(_didChangeTextEditingValue);
widget.focusNode.addListener(_handleFocusChanged); widget.focusNode.addListener(_handleFocusChanged);
...@@ -1808,12 +1807,14 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1808,12 +1807,14 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
_internalScrollController?.dispose(); _internalScrollController?.dispose();
_currentAutofillScope?.unregister(autofillId); _currentAutofillScope?.unregister(autofillId);
widget.controller.removeListener(_didChangeTextEditingValue); widget.controller.removeListener(_didChangeTextEditingValue);
_floatingCursorResetController.dispose(); _floatingCursorResetController?.dispose();
_floatingCursorResetController = null;
_closeInputConnectionIfNeeded(); _closeInputConnectionIfNeeded();
assert(!_hasInputConnection); assert(!_hasInputConnection);
_cursorTimer?.cancel(); _cursorTimer?.cancel();
_cursorTimer = null; _cursorTimer = null;
_cursorBlinkOpacityController.dispose(); _cursorBlinkOpacityController?.dispose();
_cursorBlinkOpacityController = null;
_selectionOverlay?.dispose(); _selectionOverlay?.dispose();
_selectionOverlay = null; _selectionOverlay = null;
widget.focusNode.removeListener(_handleFocusChanged); widget.focusNode.removeListener(_handleFocusChanged);
...@@ -1952,10 +1953,13 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1952,10 +1953,13 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
@override @override
void updateFloatingCursor(RawFloatingCursorPoint point) { void updateFloatingCursor(RawFloatingCursorPoint point) {
_floatingCursorResetController ??= AnimationController(
vsync: this,
)..addListener(_onFloatingCursorResetTick);
switch(point.state) { switch(point.state) {
case FloatingCursorDragState.Start: case FloatingCursorDragState.Start:
if (_floatingCursorResetController.isAnimating) { if (_floatingCursorResetController!.isAnimating) {
_floatingCursorResetController.stop(); _floatingCursorResetController!.stop();
_onFloatingCursorResetTick(); _onFloatingCursorResetTick();
} }
// We want to send in points that are centered around a (0,0) origin, so // We want to send in points that are centered around a (0,0) origin, so
...@@ -1980,8 +1984,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1980,8 +1984,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
case FloatingCursorDragState.End: case FloatingCursorDragState.End:
// We skip animation if no update has happened. // We skip animation if no update has happened.
if (_lastTextPosition != null && _lastBoundedOffset != null) { if (_lastTextPosition != null && _lastBoundedOffset != null) {
_floatingCursorResetController.value = 0.0; _floatingCursorResetController!.value = 0.0;
_floatingCursorResetController.animateTo(1.0, duration: _floatingCursorResetTime, curve: Curves.decelerate); _floatingCursorResetController!.animateTo(1.0, duration: _floatingCursorResetTime, curve: Curves.decelerate);
} }
break; break;
} }
...@@ -1989,7 +1993,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1989,7 +1993,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
void _onFloatingCursorResetTick() { void _onFloatingCursorResetTick() {
final Offset finalPosition = renderEditable.getLocalRectForCaret(_lastTextPosition!).centerLeft - _floatingCursorOffset; final Offset finalPosition = renderEditable.getLocalRectForCaret(_lastTextPosition!).centerLeft - _floatingCursorOffset;
if (_floatingCursorResetController.isCompleted) { if (_floatingCursorResetController!.isCompleted) {
renderEditable.setFloatingCursor(FloatingCursorDragState.End, finalPosition, _lastTextPosition!); renderEditable.setFloatingCursor(FloatingCursorDragState.End, finalPosition, _lastTextPosition!);
if (_lastTextPosition!.offset != renderEditable.selection!.baseOffset) if (_lastTextPosition!.offset != renderEditable.selection!.baseOffset)
// The cause is technically the force cursor, but the cause is listed as tap as the desired functionality is the same. // The cause is technically the force cursor, but the cause is listed as tap as the desired functionality is the same.
...@@ -1999,7 +2003,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1999,7 +2003,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
_pointOffsetOrigin = null; _pointOffsetOrigin = null;
_lastBoundedOffset = null; _lastBoundedOffset = null;
} else { } else {
final double lerpValue = _floatingCursorResetController.value; final double lerpValue = _floatingCursorResetController!.value;
final double lerpX = ui.lerpDouble(_lastBoundedOffset!.dx, finalPosition.dx, lerpValue)!; final double lerpX = ui.lerpDouble(_lastBoundedOffset!.dx, finalPosition.dx, lerpValue)!;
final double lerpY = ui.lerpDouble(_lastBoundedOffset!.dy, finalPosition.dy, lerpValue)!; final double lerpY = ui.lerpDouble(_lastBoundedOffset!.dy, finalPosition.dy, lerpValue)!;
...@@ -2528,14 +2532,14 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2528,14 +2532,14 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
} }
void _onCursorColorTick() { void _onCursorColorTick() {
renderEditable.cursorColor = widget.cursorColor.withOpacity(_cursorBlinkOpacityController.value); renderEditable.cursorColor = widget.cursorColor.withOpacity(_cursorBlinkOpacityController!.value);
_cursorVisibilityNotifier.value = widget.showCursor && _cursorBlinkOpacityController.value > 0; _cursorVisibilityNotifier.value = widget.showCursor && _cursorBlinkOpacityController!.value > 0;
} }
/// Whether the blinking cursor is actually visible at this precise moment /// Whether the blinking cursor is actually visible at this precise moment
/// (it's hidden half the time, since it blinks). /// (it's hidden half the time, since it blinks).
@visibleForTesting @visibleForTesting
bool get cursorCurrentlyVisible => _cursorBlinkOpacityController.value > 0; bool get cursorCurrentlyVisible => _cursorBlinkOpacityController!.value > 0;
/// The cursor blink interval (the amount of time the cursor is in the "on" /// The cursor blink interval (the amount of time the cursor is in the "on"
/// state or the "off" state). A complete cursor blink period is twice this /// state or the "off" state). A complete cursor blink period is twice this
...@@ -2561,9 +2565,9 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2561,9 +2565,9 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
// //
// These values and curves have been obtained through eyeballing, so are // These values and curves have been obtained through eyeballing, so are
// likely not exactly the same as the values for native iOS. // likely not exactly the same as the values for native iOS.
_cursorBlinkOpacityController.animateTo(targetOpacity, curve: Curves.easeOut); _cursorBlinkOpacityController!.animateTo(targetOpacity, curve: Curves.easeOut);
} else { } else {
_cursorBlinkOpacityController.value = targetOpacity; _cursorBlinkOpacityController!.value = targetOpacity;
} }
if (_obscureShowCharTicksPending > 0) { if (_obscureShowCharTicksPending > 0) {
...@@ -2591,7 +2595,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2591,7 +2595,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
return; return;
} }
_targetCursorVisibility = true; _targetCursorVisibility = true;
_cursorBlinkOpacityController.value = 1.0; _cursorBlinkOpacityController!.value = 1.0;
if (EditableText.debugDeterministicCursor) if (EditableText.debugDeterministicCursor)
return; return;
if (widget.cursorOpacityAnimates) { if (widget.cursorOpacityAnimates) {
...@@ -2606,14 +2610,14 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2606,14 +2610,14 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
_cursorTimer?.cancel(); _cursorTimer?.cancel();
_cursorTimer = null; _cursorTimer = null;
_targetCursorVisibility = false; _targetCursorVisibility = false;
_cursorBlinkOpacityController.value = 0.0; _cursorBlinkOpacityController!.value = 0.0;
if (EditableText.debugDeterministicCursor) if (EditableText.debugDeterministicCursor)
return; return;
if (resetCharTicks) if (resetCharTicks)
_obscureShowCharTicksPending = 0; _obscureShowCharTicksPending = 0;
if (widget.cursorOpacityAnimates) { if (widget.cursorOpacityAnimates) {
_cursorBlinkOpacityController.stop(); _cursorBlinkOpacityController!.stop();
_cursorBlinkOpacityController.value = 0.0; _cursorBlinkOpacityController!.value = 0.0;
} }
} }
......
...@@ -15,7 +15,7 @@ export 'package:flutter/scheduler.dart' show TickerProvider; ...@@ -15,7 +15,7 @@ export 'package:flutter/scheduler.dart' show TickerProvider;
/// This only works if [AnimationController] objects are created using /// This only works if [AnimationController] objects are created using
/// widget-aware ticker providers. For example, using a /// widget-aware ticker providers. For example, using a
/// [TickerProviderStateMixin] or a [SingleTickerProviderStateMixin]. /// [TickerProviderStateMixin] or a [SingleTickerProviderStateMixin].
class TickerMode extends StatelessWidget { class TickerMode extends StatefulWidget {
/// Creates a widget that enables or disables tickers. /// Creates a widget that enables or disables tickers.
/// ///
/// The [enabled] argument must not be null. /// The [enabled] argument must not be null.
...@@ -62,18 +62,85 @@ class TickerMode extends StatelessWidget { ...@@ -62,18 +62,85 @@ class TickerMode extends StatelessWidget {
return widget?.enabled ?? true; return widget?.enabled ?? true;
} }
/// Obtains a [ValueNotifier] from the [TickerMode] surrounding the `context`,
/// which indicates whether tickers are enabled in the given subtree.
///
/// When that [TickerMode] enabled or disabled tickers, the notifier notifies
/// its listeners.
///
/// While the [ValueNotifier] 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 changes from true to false or vice
/// versa. This is preferable when the ticker mode does not impact what is
/// currently rendered on screen, e.g. because it is ony 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 notifier,
/// 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
/// [ValueNotifier], whose [ValueNotifier.value] is always true.
static ValueNotifier<bool> getNotifier(BuildContext context) {
final _EffectiveTickerMode? widget = context.getElementForInheritedWidgetOfExactType<_EffectiveTickerMode>()?.widget as _EffectiveTickerMode?;
return widget?.notifier ?? ValueNotifier<bool>(true);
}
@override
State<TickerMode> createState() => _TickerModeState();
}
class _TickerModeState extends State<TickerMode> {
bool _ancestorTicketMode = true;
final ValueNotifier<bool> _effectiveMode = ValueNotifier<bool>(true);
@override
void didChangeDependencies() {
super.didChangeDependencies();
_ancestorTicketMode = TickerMode.of(context);
_updateEffectiveMode();
}
@override
void didUpdateWidget(TickerMode oldWidget) {
super.didUpdateWidget(oldWidget);
_updateEffectiveMode();
}
@override
void dispose() {
_effectiveMode.dispose();
super.dispose();
}
void _updateEffectiveMode() {
_effectiveMode.value = _ancestorTicketMode && widget.enabled;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return _EffectiveTickerMode( return _EffectiveTickerMode(
enabled: enabled && TickerMode.of(context), enabled: _effectiveMode.value,
child: child, notifier: _effectiveMode,
child: widget.child,
); );
} }
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties); super.debugFillProperties(properties);
properties.add(FlagProperty('requested mode', value: enabled, ifTrue: 'enabled', ifFalse: 'disabled', showName: true)); properties.add(FlagProperty('requested mode', value: widget.enabled, ifTrue: 'enabled', ifFalse: 'disabled', showName: true));
} }
} }
...@@ -81,11 +148,13 @@ class _EffectiveTickerMode extends InheritedWidget { ...@@ -81,11 +148,13 @@ class _EffectiveTickerMode extends InheritedWidget {
const _EffectiveTickerMode({ const _EffectiveTickerMode({
Key? key, Key? key,
required this.enabled, required this.enabled,
required this.notifier,
required Widget child, required Widget child,
}) : assert(enabled != null), }) : assert(enabled != null),
super(key: key, child: child); super(key: key, child: child);
final bool enabled; final bool enabled;
final ValueNotifier<bool> notifier;
@override @override
bool updateShouldNotify(_EffectiveTickerMode oldWidget) => enabled != oldWidget.enabled; bool updateShouldNotify(_EffectiveTickerMode oldWidget) => enabled != oldWidget.enabled;
...@@ -127,10 +196,8 @@ mixin SingleTickerProviderStateMixin<T extends StatefulWidget> on State<T> imple ...@@ -127,10 +196,8 @@ mixin SingleTickerProviderStateMixin<T extends StatefulWidget> on State<T> imple
]); ]);
}()); }());
_ticker = Ticker(onTick, debugLabel: kDebugMode ? 'created by ${describeIdentity(this)}' : null); _ticker = Ticker(onTick, debugLabel: kDebugMode ? 'created by ${describeIdentity(this)}' : null);
// We assume that this is called from initState, build, or some sort of _updateTickerModeNotifier();
// event handler, and that thus TickerMode.of(context) would return true. We _updateTicker(); // Sets _ticker.mute correctly.
// can't actually check that here because if we're in initState then we're
// not allowed to do inheritance checks yet.
return _ticker!; return _ticker!;
} }
...@@ -154,14 +221,35 @@ mixin SingleTickerProviderStateMixin<T extends StatefulWidget> on State<T> imple ...@@ -154,14 +221,35 @@ mixin SingleTickerProviderStateMixin<T extends StatefulWidget> on State<T> imple
_ticker!.describeForError('The offending ticker was'), _ticker!.describeForError('The offending ticker was'),
]); ]);
}()); }());
_tickerModeNotifier?.removeListener(_updateTicker);
_tickerModeNotifier = null;
super.dispose(); super.dispose();
} }
ValueNotifier<bool>? _tickerModeNotifier;
@override @override
void didChangeDependencies() { void activate() {
if (_ticker != null) super.activate();
_ticker!.muted = !TickerMode.of(context); // We may have a new TickerMode ancestor.
super.didChangeDependencies(); _updateTickerModeNotifier();
_updateTicker();
}
void _updateTicker() {
if (_ticker != null) {
_ticker!.muted = !_tickerModeNotifier!.value;
}
}
void _updateTickerModeNotifier() {
final ValueNotifier<bool> newNotifier = TickerMode.getNotifier(context);
if (newNotifier == _tickerModeNotifier) {
return;
}
_tickerModeNotifier?.removeListener(_updateTicker);
newNotifier.addListener(_updateTicker);
_tickerModeNotifier = newNotifier;
} }
@override @override
...@@ -198,8 +286,14 @@ mixin TickerProviderStateMixin<T extends StatefulWidget> on State<T> implements ...@@ -198,8 +286,14 @@ mixin TickerProviderStateMixin<T extends StatefulWidget> on State<T> implements
@override @override
Ticker createTicker(TickerCallback onTick) { Ticker createTicker(TickerCallback onTick) {
if (_tickerModeNotifier == null) {
// Setup TickerMode notifier before we vend the first ticker.
_updateTickerModeNotifier();
}
assert(_tickerModeNotifier != null);
_tickers ??= <_WidgetTicker>{}; _tickers ??= <_WidgetTicker>{};
final _WidgetTicker result = _WidgetTicker(onTick, this, debugLabel: kDebugMode ? 'created by ${describeIdentity(this)}' : null); final _WidgetTicker result = _WidgetTicker(onTick, this, debugLabel: kDebugMode ? 'created by ${describeIdentity(this)}' : null)
..muted = !_tickerModeNotifier!.value;
_tickers!.add(result); _tickers!.add(result);
return result; return result;
} }
...@@ -210,6 +304,35 @@ mixin TickerProviderStateMixin<T extends StatefulWidget> on State<T> implements ...@@ -210,6 +304,35 @@ mixin TickerProviderStateMixin<T extends StatefulWidget> on State<T> implements
_tickers!.remove(ticker); _tickers!.remove(ticker);
} }
ValueNotifier<bool>? _tickerModeNotifier;
@override
void activate() {
super.activate();
// We may have a new TickerMode ancestor, get its Notifier.
_updateTickerModeNotifier();
_updateTickers();
}
void _updateTickers() {
if (_tickers != null) {
final bool muted = !_tickerModeNotifier!.value;
for (final Ticker ticker in _tickers!) {
ticker.muted = muted;
}
}
}
void _updateTickerModeNotifier() {
final ValueNotifier<bool> newNotifier = TickerMode.getNotifier(context);
if (newNotifier == _tickerModeNotifier) {
return;
}
_tickerModeNotifier?.removeListener(_updateTickers);
newNotifier.addListener(_updateTickers);
_tickerModeNotifier = newNotifier;
}
@override @override
void dispose() { void dispose() {
assert(() { assert(() {
...@@ -235,20 +358,11 @@ mixin TickerProviderStateMixin<T extends StatefulWidget> on State<T> implements ...@@ -235,20 +358,11 @@ mixin TickerProviderStateMixin<T extends StatefulWidget> on State<T> implements
} }
return true; return true;
}()); }());
_tickerModeNotifier?.removeListener(_updateTickers);
_tickerModeNotifier = null;
super.dispose(); super.dispose();
} }
@override
void didChangeDependencies() {
final bool muted = !TickerMode.of(context);
if (_tickers != null) {
for (final Ticker ticker in _tickers!) {
ticker.muted = muted;
}
}
super.didChangeDependencies();
}
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties); super.debugFillProperties(properties);
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
...@@ -98,36 +99,194 @@ void main() { ...@@ -98,36 +99,194 @@ void main() {
expect(outerTickCount, 0); expect(outerTickCount, 0);
expect(innerTickCount, 0); expect(innerTickCount, 0);
}); });
testWidgets('Changing TickerMode does not rebuild widgets with SingleTickerProviderStateMixin', (WidgetTester tester) async {
Widget widgetUnderTest({required bool tickerEnabled}) {
return TickerMode(
enabled: tickerEnabled,
child: const _TickingWidget(),
);
}
_TickingWidgetState state() => tester.state<_TickingWidgetState>(find.byType(_TickingWidget));
await tester.pumpWidget(widgetUnderTest(tickerEnabled: true));
expect(state().ticker.isTicking, isTrue);
expect(state().buildCount, 1);
await tester.pumpWidget(widgetUnderTest(tickerEnabled: false));
expect(state().ticker.isTicking, isFalse);
expect(state().buildCount, 1);
await tester.pumpWidget(widgetUnderTest(tickerEnabled: true));
expect(state().ticker.isTicking, isTrue);
expect(state().buildCount, 1);
});
testWidgets('Changing TickerMode does not rebuild widgets with TickerProviderStateMixin', (WidgetTester tester) async {
Widget widgetUnderTest({required bool tickerEnabled}) {
return TickerMode(
enabled: tickerEnabled,
child: const _MultiTickingWidget(),
);
}
_MultiTickingWidgetState state() => tester.state<_MultiTickingWidgetState>(find.byType(_MultiTickingWidget));
await tester.pumpWidget(widgetUnderTest(tickerEnabled: true));
expect(state().ticker.isTicking, isTrue);
expect(state().buildCount, 1);
await tester.pumpWidget(widgetUnderTest(tickerEnabled: false));
expect(state().ticker.isTicking, isFalse);
expect(state().buildCount, 1);
await tester.pumpWidget(widgetUnderTest(tickerEnabled: true));
expect(state().ticker.isTicking, isTrue);
expect(state().buildCount, 1);
});
testWidgets('Moving widgets with SingleTickerProviderStateMixin to a new TickerMode ancestor works', (WidgetTester tester) async {
final GlobalKey tickingWidgetKey = GlobalKey();
Widget widgetUnderTest({required LocalKey tickerModeKey, required bool tickerEnabled}) {
return TickerMode(
key: tickerModeKey,
enabled: tickerEnabled,
child: _TickingWidget(key: tickingWidgetKey),
);
}
// Using different local keys to simulate changing TickerMode ancestors.
await tester.pumpWidget(widgetUnderTest(tickerEnabled: true, tickerModeKey: UniqueKey()));
final State tickerModeState = tester.state(find.byType(TickerMode));
final _TickingWidgetState tickingState = tester.state<_TickingWidgetState>(find.byType(_TickingWidget));
expect(tickingState.ticker.isTicking, isTrue);
await tester.pumpWidget(widgetUnderTest(tickerEnabled: false, tickerModeKey: UniqueKey()));
expect(tester.state(find.byType(TickerMode)), isNot(same(tickerModeState)));
expect(tickingState, same(tester.state<_TickingWidgetState>(find.byType(_TickingWidget))));
expect(tickingState.ticker.isTicking, isFalse);
});
testWidgets('Moving widgets with TickerProviderStateMixin to a new TickerMode ancestor works', (WidgetTester tester) async {
final GlobalKey tickingWidgetKey = GlobalKey();
Widget widgetUnderTest({required LocalKey tickerModeKey, required bool tickerEnabled}) {
return TickerMode(
key: tickerModeKey,
enabled: tickerEnabled,
child: _MultiTickingWidget(key: tickingWidgetKey),
);
}
// Using different local keys to simulate changing TickerMode ancestors.
await tester.pumpWidget(widgetUnderTest(tickerEnabled: true, tickerModeKey: UniqueKey()));
final State tickerModeState = tester.state(find.byType(TickerMode));
final _MultiTickingWidgetState tickingState = tester.state<_MultiTickingWidgetState>(find.byType(_MultiTickingWidget));
expect(tickingState.ticker.isTicking, isTrue);
await tester.pumpWidget(widgetUnderTest(tickerEnabled: false, tickerModeKey: UniqueKey()));
expect(tester.state(find.byType(TickerMode)), isNot(same(tickerModeState)));
expect(tickingState, same(tester.state<_MultiTickingWidgetState>(find.byType(_MultiTickingWidget))));
expect(tickingState.ticker.isTicking, isFalse);
});
testWidgets('Ticking widgets in old route do not rebuild when new route is pushed', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
routes: <String, WidgetBuilder>{
'/foo' : (BuildContext context) => const Text('New route'),
},
home: Row(
children: const <Widget>[
_TickingWidget(),
_MultiTickingWidget(),
Text('Old route'),
],
),
));
_MultiTickingWidgetState multiTickingState() => tester.state<_MultiTickingWidgetState>(find.byType(_MultiTickingWidget, skipOffstage: false));
_TickingWidgetState tickingState() => tester.state<_TickingWidgetState>(find.byType(_TickingWidget, skipOffstage: false));
expect(find.text('Old route'), findsOneWidget);
expect(find.text('New route'), findsNothing);
expect(multiTickingState().ticker.isTicking, isTrue);
expect(multiTickingState().buildCount, 1);
expect(tickingState().ticker.isTicking, isTrue);
expect(tickingState().buildCount, 1);
tester.state<NavigatorState>(find.byType(Navigator)).pushNamed('/foo');
await tester.pumpAndSettle();
expect(find.text('Old route'), findsNothing);
expect(find.text('New route'), findsOneWidget);
expect(multiTickingState().ticker.isTicking, isFalse);
expect(multiTickingState().buildCount, 1);
expect(tickingState().ticker.isTicking, isFalse);
expect(tickingState().buildCount, 1);
});
} }
class _TickingWidget extends StatefulWidget { class _TickingWidget extends StatefulWidget {
const _TickingWidget({required this.onTick}); const _TickingWidget({Key? key, this.onTick}) : super(key: key);
final VoidCallback onTick; final VoidCallback? onTick;
@override @override
State<_TickingWidget> createState() => _TickingWidgetState(); State<_TickingWidget> createState() => _TickingWidgetState();
} }
class _TickingWidgetState extends State<_TickingWidget> with SingleTickerProviderStateMixin { class _TickingWidgetState extends State<_TickingWidget> with SingleTickerProviderStateMixin {
late Ticker _ticker; late Ticker ticker;
int buildCount = 0;
@override
void initState() {
super.initState();
ticker = createTicker((Duration _) {
widget.onTick?.call();
})..start();
}
@override
Widget build(BuildContext context) {
buildCount += 1;
return Container();
}
@override
void dispose() {
ticker.dispose();
super.dispose();
}
}
class _MultiTickingWidget extends StatefulWidget {
const _MultiTickingWidget({Key? key, this.onTick}) : super(key: key);
final VoidCallback? onTick;
@override
State<_MultiTickingWidget> createState() => _MultiTickingWidgetState();
}
class _MultiTickingWidgetState extends State<_MultiTickingWidget> with TickerProviderStateMixin {
late Ticker ticker;
int buildCount = 0;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_ticker = createTicker((Duration _) { ticker = createTicker((Duration _) {
widget.onTick(); widget.onTick?.call();
})..start(); })..start();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
buildCount += 1;
return Container(); return Container();
} }
@override @override
void dispose() { void dispose() {
_ticker.dispose(); ticker.dispose();
super.dispose(); super.dispose();
} }
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment