main
dart 110 lines 3.25 KB
Raw
1 // ignore_for_file: implementation_imports, invalid_use_of_internal_member
2
3 import 'dart:async';
4
5 import 'package:flutter/material.dart';
6 import 'package:flutter/scheduler.dart';
7 import 'package:flutter_riverpod/src/internals.dart';
8
9 /// A [Vsync] proxy that intercepts Riverpod task scheduling and safely defers
10 /// refreshes to the next event loop iteration if Flutter is currently in the
11 /// middle of a build or render pass (`SchedulerPhase.persistentCallbacks`).
12 ///
13 /// In Riverpod 3.x, out-of-view widgets are paused by default. When routes pop
14 /// or transition, [TickerMode] toggles during Flutter's build phase
15 /// (`_TickerModeState.didUpdateWidget`), synchronously resuming paused
16 /// subscriptions. Resuming flushes providers that changed while paused.
17 /// If any dependent provider invalidates during flush, Riverpod's default
18 /// [UncontrolledProviderScope] vsync invokes `setState()` synchronously,
19 /// crashing Flutter with "setState() or markNeedsBuild() called during build".
20 ///
21 /// [SafeRiverpodVsync] eliminates this by checking if Flutter is currently
22 /// building, and using [Timer.zero] to schedule the refresh immediately after
23 /// the frame concludes.
24 class SafeRiverpodVsync implements Vsync {
25 final Vsync delegate;
26
27 SafeRiverpodVsync(this.delegate);
28
29 @override
30 void Function()? scheduleRefresh(Task task) {
31 final phase = SchedulerBinding.instance.schedulerPhase;
32 final isBuilding =
33 phase == SchedulerPhase.persistentCallbacks ||
34 WidgetsBinding.instance.buildOwner?.debugBuilding == true;
35
36 if (isBuilding) {
37 final timer = Timer(Duration.zero, task.call);
38 return timer.cancel;
39 }
40
41 try {
42 return delegate.scheduleRefresh(task);
43 } catch (_) {
44 final timer = Timer(Duration.zero, task.call);
45 return timer.cancel;
46 }
47 }
48
49 @override
50 void Function()? scheduleDispose(Task task) {
51 return delegate.scheduleDispose(task);
52 }
53 }
54
55 /// Wraps the child of [UncontrolledProviderScope] and ensures all registered
56 /// vsyncs in [ProviderContainer.scheduler.flutterVsyncs] are protected by
57 /// [SafeRiverpodVsync].
58 class SafeRiverpodScopeHost extends StatefulWidget {
59 final ProviderContainer container;
60 final Widget child;
61
62 const SafeRiverpodScopeHost({
63 super.key,
64 required this.container,
65 required this.child,
66 });
67
68 @override
69 State<SafeRiverpodScopeHost> createState() => _SafeRiverpodScopeHostState();
70 }
71
72 class _SafeRiverpodScopeHostState extends State<SafeRiverpodScopeHost> {
73 @override
74 void initState() {
75 super.initState();
76 _protectVsyncs();
77 }
78
79 @override
80 void didUpdateWidget(SafeRiverpodScopeHost oldWidget) {
81 super.didUpdateWidget(oldWidget);
82 _protectVsyncs();
83 }
84
85 void _protectVsyncs() {
86 final scheduler = widget.container.scheduler;
87 final vsyncs = scheduler.flutterVsyncs.toList();
88 var hasUnwrapped = false;
89 for (final v in vsyncs) {
90 if (v is! SafeRiverpodVsync) {
91 hasUnwrapped = true;
92 break;
93 }
94 }
95 if (hasUnwrapped) {
96 scheduler.flutterVsyncs.clear();
97 for (final v in vsyncs) {
98 scheduler.flutterVsyncs.add(
99 v is SafeRiverpodVsync ? v : SafeRiverpodVsync(v),
100 );
101 }
102 }
103 }
104
105 @override
106 Widget build(BuildContext context) {
107 _protectVsyncs();
108 return widget.child;
109 }
110 }