Skip to content

Commit

Permalink
Fix vsync race condition on providerscope unmount (#3072)
Browse files Browse the repository at this point in the history
fixes #2022
  • Loading branch information
rrousselGit authored Oct 31, 2023
1 parent 66e7777 commit dd3c7df
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 10 deletions.
1 change: 1 addition & 0 deletions packages/flutter_riverpod/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

- Exceptions in asynchronous providers are now correctly received
by `ProviderObserver.providerDidFail`.
- Fix exception when a `ProviderScope` is rebuilt with a different `key`.

## 2.4.5 - 2023-10-28

Expand Down
6 changes: 2 additions & 4 deletions packages/flutter_riverpod/lib/src/framework.dart
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ class _UncontrolledProviderScopeElement extends InheritedElement {
debugCanModifyProviders ??= _debugCanModifyProviders;
}

vsyncOverride ??= _flutterVsync;
flutterVsyncs.add(_flutterVsync);
super.mount(parent, newSlot);
}

Expand Down Expand Up @@ -380,9 +380,7 @@ To fix this problem, you have one of two solutions:
debugCanModifyProviders = null;
}

if (vsyncOverride == _flutterVsync) {
vsyncOverride = null;
}
flutterVsyncs.remove(_flutterVsync);

super.unmount();
}
Expand Down
103 changes: 100 additions & 3 deletions packages/flutter_riverpod/test/framework_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,43 @@ import 'package:mockito/mockito.dart';
import 'utils.dart';

void main() {
testWidgets('Supports recreating ProviderScope', (tester) async {
final provider = Provider<String>((ref) => 'foo');

await tester.pumpWidget(
ProviderScope(
child: Consumer(
builder: (context, ref, _) {
ref.watch(provider);
return Consumer(
builder: (context, ref, _) {
ref.watch(provider);
return Container();
},
);
},
),
),
);

await tester.pumpWidget(
ProviderScope(
key: UniqueKey(),
child: Consumer(
builder: (context, ref, _) {
ref.watch(provider);
return Consumer(
builder: (context, ref, _) {
ref.watch(provider);
return Container();
},
);
},
),
),
);
});

testWidgets('ref.invalidate can invalidate a family', (tester) async {
final listener = Listener<String>();
final listener2 = Listener<String>();
Expand Down Expand Up @@ -248,7 +285,27 @@ void main() {
(tester) async {
final container = createContainer();

expect(vsyncOverride, null);
expect(flutterVsyncs, isEmpty);

await tester.pumpWidget(
UncontrolledProviderScope(
container: container,
child: Container(),
),
);

expect(flutterVsyncs, hasLength(1));

await tester.pumpWidget(
UncontrolledProviderScope(
container: container,
child: ProviderScope(
child: Container(),
),
),
);

expect(flutterVsyncs, hasLength(2));

await tester.pumpWidget(
UncontrolledProviderScope(
Expand All @@ -257,11 +314,51 @@ void main() {
),
);

expect(vsyncOverride, isNotNull);
expect(flutterVsyncs, hasLength(1));

await tester.pumpWidget(Container());

expect(vsyncOverride, null);
expect(flutterVsyncs, isEmpty);
});

testWidgets('When there are multiple vsyncs, rebuild providers only once',
(tester) async {
var buildCount = 0;
final dep = StateProvider((ref) => 0);
final provider = Provider((ref) {
buildCount++;
return ref.watch(dep);
});

await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ProviderScope(
child: ProviderScope(
child: Consumer(
builder: (context, ref, _) {
return Text('Hello ${ref.watch(provider)}');
},
),
),
),
),
);

expect(buildCount, 1);
expect(find.text('Hello 0'), findsOneWidget);
expect(find.text('Hello 1'), findsNothing);

final consumerElement = tester.element(find.byType(Consumer));
final container = ProviderScope.containerOf(consumerElement);

container.read(dep.notifier).state++;

await tester.pump();

expect(buildCount, 2);
expect(find.text('Hello 1'), findsOneWidget);
expect(find.text('Hello 0'), findsNothing);
});

testWidgets(
Expand Down
1 change: 1 addition & 0 deletions packages/riverpod/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

- Exceptions in asynchronous providers are now correctly received
by `ProviderObserver.providerDidFail`.
- Fix exception when a `ProviderScope` is rebuilt with a different `key`.

## 2.4.5 - 2023-10-28

Expand Down
3 changes: 2 additions & 1 deletion packages/riverpod/lib/riverpod.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ export 'src/framework.dart'
hide
debugCanModifyProviders,
vsync,
vsyncOverride,
flutterVsyncs,
Vsync,
ValueProviderElement,
ValueProvider,
FamilyCreate,
Expand Down
27 changes: 25 additions & 2 deletions packages/riverpod/lib/src/framework/scheduler.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
part of '../framework.dart';

/// A listener used to determine when providers should rebuild.
/// This is used to synchronize provider rebuilds when widget rebuilds.
@internal
typedef Vsync = void Function(void Function());

/// A way to override [vsync], used by Flutter to synchronize a container
/// with the widget tree.
@internal
void Function(void Function() task)? vsyncOverride;
final flutterVsyncs = <Vsync>{};

void _defaultVsync(void Function() task) {
Future(task);
Expand All @@ -14,7 +19,25 @@ void _defaultVsync(void Function() task) {
/// Defaults to refreshing providers at the end of the next event-loop.
@internal
void Function(void Function()) get vsync {
return vsyncOverride ?? _defaultVsync;
if (flutterVsyncs.isNotEmpty) {
// Notify all InheritedWidgets of a possible rebuild.
// At the same time, we only execute the task once, in whichever
// InheritedWidget that rebuilds first.
return (task) {
var invoked = false;
void invoke() {
if (invoked) return;
invoked = true;
task();
}

for (final flutterVsync in flutterVsyncs) {
flutterVsync(invoke);
}
};
}

return _defaultVsync;
}

/// The object that handles when providers are refreshed and disposed.
Expand Down

0 comments on commit dd3c7df

Please sign in to comment.