Skip to content

Commit 2305423

Browse files
ViceVice
authored andcommitted
- useMultiTickerProvider
1 parent 352aeb3 commit 2305423

File tree

2 files changed

+217
-0
lines changed

2 files changed

+217
-0
lines changed

packages/flutter_hooks/lib/src/animation.dart

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,3 +237,95 @@ class _TickerProviderHookState
237237
@override
238238
bool get debugSkipValue => true;
239239
}
240+
241+
/// Creates a [TickerProvider] that supports creating multiple [Ticker]s.
242+
///
243+
/// See also:
244+
/// * [SingleTickerProviderStateMixin]
245+
TickerProvider useMultiTickerProvider({List<Object?>? keys}) {
246+
return use(
247+
keys != null
248+
? _MultiTickerProviderHook(keys)
249+
: const _MultiTickerProviderHook(),
250+
);
251+
}
252+
253+
class _MultiTickerProviderHook extends Hook<TickerProvider> {
254+
const _MultiTickerProviderHook([List<Object?>? keys]) : super(keys: keys);
255+
256+
@override
257+
_MultiTickerProviderHookState createState() => _MultiTickerProviderHookState();
258+
}
259+
260+
class _MultiTickerProviderHookState
261+
extends HookState<TickerProvider, _MultiTickerProviderHook>
262+
implements TickerProvider {
263+
final Set<Ticker> _tickers = <Ticker>{};
264+
ValueListenable<bool>? _tickerModeNotifier;
265+
266+
@override
267+
Ticker createTicker(TickerCallback onTick) {
268+
final ticker = Ticker(onTick, debugLabel: 'created by $context (multi)');
269+
_updateTickerModeNotifier();
270+
_updateTickers();
271+
_tickers.add(ticker);
272+
return ticker;
273+
}
274+
275+
@override
276+
void dispose() {
277+
assert(() {
278+
// Ensure there are no active tickers left. Controllers that own Tickers
279+
// are responsible for disposing them — leaving an active ticker here is
280+
// almost always a leak or misuse.
281+
for (final t in _tickers) {
282+
if (t.isActive) {
283+
throw FlutterError(
284+
'useMultiTickerProvider created Ticker(s), but at the time '
285+
'dispose() was called on the Hook, at least one of those Tickers '
286+
'was still active. Tickers used by AnimationControllers should '
287+
'be disposed by calling dispose() on the AnimationController '
288+
'itself. Otherwise, the ticker will leak.\n');
289+
}
290+
}
291+
return true;
292+
}(), '');
293+
294+
_tickerModeNotifier?.removeListener(_updateTickers);
295+
_tickerModeNotifier = null;
296+
_tickers.clear();
297+
super.dispose();
298+
}
299+
300+
@override
301+
TickerProvider build(BuildContext context) {
302+
_updateTickerModeNotifier();
303+
_updateTickers();
304+
return this;
305+
}
306+
307+
void _updateTickers() {
308+
if (_tickers.isNotEmpty) {
309+
final muted = !(_tickerModeNotifier?.value ?? TickerMode.of(context));
310+
for (final t in _tickers) {
311+
t.muted = muted;
312+
}
313+
}
314+
}
315+
316+
void _updateTickerModeNotifier() {
317+
final newNotifier = TickerMode.getNotifier(context);
318+
if (newNotifier == _tickerModeNotifier) {
319+
return;
320+
}
321+
_tickerModeNotifier?.removeListener(_updateTickers);
322+
newNotifier.addListener(_updateTickers);
323+
_tickerModeNotifier = newNotifier;
324+
}
325+
326+
@override
327+
String get debugLabel => 'useMultiTickerProvider';
328+
329+
@override
330+
bool get debugSkipValue => true;
331+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import 'dart:async';
2+
3+
import 'package:flutter/foundation.dart';
4+
import 'package:flutter/widgets.dart';
5+
import 'package:flutter_hooks/flutter_hooks.dart';
6+
7+
import 'mock.dart';
8+
9+
void main() {
10+
testWidgets('debugFillProperties', (tester) async {
11+
await tester.pumpWidget(
12+
HookBuilder(builder: (context) {
13+
useMultiTickerProvider();
14+
return const SizedBox();
15+
}),
16+
);
17+
18+
await tester.pump();
19+
20+
final element = tester.element(find.byType(HookBuilder));
21+
22+
expect(
23+
element
24+
.toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage)
25+
.toStringDeep(),
26+
equalsIgnoringHashCodes(
27+
'HookBuilder\n'
28+
' │ useMultiTickerProvider\n'
29+
' └SizedBox(renderObject: RenderConstrainedBox#00000)\n',
30+
),
31+
);
32+
});
33+
34+
testWidgets('useMultiTickerProvider basic', (tester) async {
35+
late TickerProvider provider;
36+
37+
await tester.pumpWidget(TickerMode(
38+
enabled: true,
39+
child: HookBuilder(builder: (context) {
40+
provider = useMultiTickerProvider();
41+
return Container();
42+
}),
43+
));
44+
45+
final animationControllerA = AnimationController(
46+
vsync: provider,
47+
duration: const Duration(seconds: 1),
48+
);
49+
final animationControllerB = AnimationController(
50+
vsync: provider,
51+
duration: const Duration(seconds: 1),
52+
);
53+
54+
unawaited(animationControllerA.forward());
55+
unawaited(animationControllerB.forward());
56+
57+
// With a multi provider, creating additional AnimationControllers is allowed.
58+
expect(
59+
() => AnimationController(vsync: provider, duration: const Duration(seconds: 1)),
60+
returnsNormally,
61+
);
62+
63+
animationControllerA.dispose();
64+
animationControllerB.dispose();
65+
66+
await tester.pumpWidget(const SizedBox());
67+
});
68+
69+
testWidgets('useMultiTickerProvider unused', (tester) async {
70+
await tester.pumpWidget(HookBuilder(builder: (context) {
71+
useMultiTickerProvider();
72+
return Container();
73+
}));
74+
75+
await tester.pumpWidget(const SizedBox());
76+
});
77+
78+
testWidgets('useMultiTickerProvider still active', (tester) async {
79+
late TickerProvider provider;
80+
81+
await tester.pumpWidget(TickerMode(
82+
enabled: true,
83+
child: HookBuilder(builder: (context) {
84+
provider = useMultiTickerProvider();
85+
return Container();
86+
}),
87+
));
88+
89+
final animationController = AnimationController(
90+
vsync: provider,
91+
duration: const Duration(seconds: 1),
92+
);
93+
94+
try {
95+
// ignore: unawaited_futures
96+
animationController.forward();
97+
98+
await tester.pumpWidget(const SizedBox());
99+
100+
expect(tester.takeException(), isFlutterError);
101+
} finally {
102+
animationController.dispose();
103+
}
104+
});
105+
106+
testWidgets('useMultiTickerProvider pass down keys', (tester) async {
107+
late TickerProvider provider;
108+
List<Object?>? keys;
109+
110+
await tester.pumpWidget(HookBuilder(builder: (context) {
111+
provider = useMultiTickerProvider(keys: keys);
112+
return Container();
113+
}));
114+
115+
final previousProvider = provider;
116+
keys = [];
117+
118+
await tester.pumpWidget(HookBuilder(builder: (context) {
119+
provider = useMultiTickerProvider(keys: keys);
120+
return Container();
121+
}));
122+
123+
expect(previousProvider, isNot(provider));
124+
});
125+
}

0 commit comments

Comments
 (0)