Skip to content

Commit

Permalink
Fix multiple root ProviderScopes
Browse files Browse the repository at this point in the history
fixes #3124
fixes #3118
  • Loading branch information
rrousselGit committed Nov 20, 2023
1 parent e31a470 commit 91acc5c
Show file tree
Hide file tree
Showing 11 changed files with 120 additions and 54 deletions.
4 changes: 4 additions & 0 deletions packages/flutter_riverpod/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## Unreleased fix

Fix exceptions when using multiple root `ProviderContainers`/`ProviderScopes`.

## 2.4.7 - 2023-11-20

- Fix `ProviderObserver.didUpdateProvider` being called with an incorrect
Expand Down
8 changes: 4 additions & 4 deletions packages/flutter_riverpod/lib/src/framework.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import 'package:meta/meta.dart';

import 'internals.dart';

/// {@template riverpod.providerscope}
/// {@template riverpod.provider_scope}
/// A widget that stores the state of providers.
///
/// All Flutter applications using Riverpod must contain a [ProviderScope] at
Expand Down Expand Up @@ -78,7 +78,7 @@ import 'internals.dart';
/// {@endtemplate}
@sealed
class ProviderScope extends StatefulWidget {
/// {@macro riverpod.providerscope}
/// {@macro riverpod.provider_scope}
const ProviderScope({
super.key,
this.overrides = const [],
Expand Down Expand Up @@ -305,7 +305,7 @@ class _UncontrolledProviderScopeElement extends InheritedElement {
debugCanModifyProviders ??= _debugCanModifyProviders;
}

flutterVsyncs.add(_flutterVsync);
_containerOf(widget).scheduler.flutterVsyncs.add(_flutterVsync);
super.mount(parent, newSlot);
}

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

flutterVsyncs.remove(_flutterVsync);
_containerOf(widget).scheduler.flutterVsyncs.remove(_flutterVsync);

super.unmount();
}
Expand Down
64 changes: 56 additions & 8 deletions packages/flutter_riverpod/test/framework_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,48 @@ void main() {
);
});

testWidgets('Supports multiple ProviderScope roots in the same tree',
(tester) async {
final a = StateProvider((_) => 0);
final b = Provider((ref) => ref.watch(a));

await tester.pumpWidget(
// No root scope. We want to test cases where there are multiple roots
Column(
mainAxisSize: MainAxisSize.min,
children: [
for (var i = 0; i < 2; i++)
SizedBox(
width: 100,
height: 100,
child: ProviderScope(
child: Consumer(
builder: (context, ref, _) {
ref.watch(a);
ref.watch(b);
return Container();
},
),
),
),
],
),
);

final containers = tester.allElements
.where((e) => e.widget is Consumer)
.map(ProviderScope.containerOf)
.toList();

expect(containers, hasLength(2));

for (final container in containers) {
container.read(a.notifier).state++;
}

await tester.pump();
});

testWidgets('ref.invalidate can invalidate a family', (tester) async {
final listener = Listener<String>();
final listener2 = Listener<String>();
Expand Down Expand Up @@ -284,8 +326,9 @@ void main() {
testWidgets('UncontrolledProviderScope gracefully handles vsync',
(tester) async {
final container = createContainer();
final container2 = createContainer(parent: container);

expect(flutterVsyncs, isEmpty);
expect(container.scheduler.flutterVsyncs, isEmpty);

await tester.pumpWidget(
UncontrolledProviderScope(
Expand All @@ -294,18 +337,21 @@ void main() {
),
);

expect(flutterVsyncs, hasLength(1));
expect(container.scheduler.flutterVsyncs, hasLength(1));
expect(container2.scheduler.flutterVsyncs, isEmpty);

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

expect(flutterVsyncs, hasLength(2));
expect(container.scheduler.flutterVsyncs, hasLength(1));
expect(container2.scheduler.flutterVsyncs, hasLength(1));

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

expect(flutterVsyncs, hasLength(1));
expect(container.scheduler.flutterVsyncs, hasLength(1));
expect(container2.scheduler.flutterVsyncs, isEmpty);

await tester.pumpWidget(Container());

expect(flutterVsyncs, isEmpty);
expect(container.scheduler.flutterVsyncs, isEmpty);
expect(container2.scheduler.flutterVsyncs, isEmpty);
});

testWidgets('When there are multiple vsyncs, rebuild providers only once',
Expand Down Expand Up @@ -625,7 +673,7 @@ void main() {
await tester.pumpWidget(
ProviderScope(
overrides: [
provider.overrideWithValue('rootoverride'),
provider.overrideWithValue('rootOverride'),
],
child: ProviderScope(
child: Consumer(
Expand All @@ -643,7 +691,7 @@ void main() {
);

expect(find.text('root root2'), findsNothing);
expect(find.text('rootoverride root2'), findsOneWidget);
expect(find.text('rootOverride root2'), findsOneWidget);
});

testWidgets('ProviderScope throws if ancestorOwner changed', (tester) async {
Expand Down
4 changes: 4 additions & 0 deletions packages/hooks_riverpod/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## Unreleased fix

Fix exceptions when using multiple root `ProviderContainers`/`ProviderScopes`.

## 2.4.7 - 2023-11-20

- Fix `ProviderObserver.didUpdateProvider` being called with an incorrect
Expand Down
4 changes: 4 additions & 0 deletions packages/riverpod/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## Unreleased fix

Fix exceptions when using multiple root `ProviderContainers`/`ProviderScopes`.

## 2.4.7 - 2023-11-20

- Fix `ProviderObserver.didUpdateProvider` being called with an incorrect
Expand Down
3 changes: 1 addition & 2 deletions packages/riverpod/lib/riverpod.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,8 @@ export 'src/common.dart' hide AsyncTransition;

export 'src/framework.dart'
hide
ProviderScheduler,
debugCanModifyProviders,
vsync,
flutterVsyncs,
Vsync,
ValueProviderElement,
ValueProvider,
Expand Down
2 changes: 1 addition & 1 deletion packages/riverpod/lib/src/framework/auto_dispose.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ mixin AutoDisposeProviderElementMixin<State> on ProviderElementBase<State>

// ignore: deprecated_member_use_from_same_package
if (!maintainState && !hasListeners && (links == null || links.isEmpty)) {
_container._scheduler.scheduleProviderDispose(this);
_container.scheduler.scheduleProviderDispose(this);
}
}

Expand Down
14 changes: 10 additions & 4 deletions packages/riverpod/lib/src/framework/container.dart
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,8 @@ class ProviderContainer implements Node {
void Function(void Function() task)? vsyncOverride;

/// The object that handles when providers are refreshed and disposed.
late final _ProviderScheduler _scheduler =
_parent?._scheduler ?? _ProviderScheduler();
@internal
late final ProviderScheduler scheduler = ProviderScheduler();

/// How deep this [ProviderContainer] is in the graph of containers.
///
Expand Down Expand Up @@ -214,7 +214,13 @@ class ProviderContainer implements Node {

/// Awaits for providers to rebuild/be disposed and for listeners to be notified.
Future<void> pump() async {
return _scheduler.pendingFuture;
final a = scheduler.pendingFuture;
final b = _parent?.scheduler.pendingFuture;

await Future.wait<void>([
if (a != null) a,
if (b != null) b,
]);
}

/// Reads a provider without listening to it and returns the currently
Expand Down Expand Up @@ -630,7 +636,7 @@ final b = Provider((ref) => ref.watch(a), dependencies: [a]);
element.dispose();
}

if (_root == null) _scheduler.dispose();
if (_root == null) scheduler.dispose();
}

/// Traverse the [ProviderElementBase]s associated with this [ProviderContainer].
Expand Down
2 changes: 1 addition & 1 deletion packages/riverpod/lib/src/framework/element.dart
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ abstract class ProviderElementBase<State> implements Ref<State>, Node {

_mustRecomputeState = true;
runOnDispose();
_container._scheduler.scheduleProviderRefresh(this);
_container.scheduler.scheduleProviderRefresh(this);

// We don't call this._markDependencyMayHaveChanged here because we voluntarily
// do not want to set the _dependencyMayHaveChanged flag to true.
Expand Down
65 changes: 33 additions & 32 deletions packages/riverpod/lib/src/framework/scheduler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,48 +5,49 @@ part of '../framework.dart';
@internal
typedef Vsync = void Function(void Function());

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

void _defaultVsync(void Function() task) {
Future(task);
}

/// A function that controls the refresh rate of providers.
///
/// Defaults to refreshing providers at the end of the next event-loop.
@internal
void Function(void Function()) get vsync {
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.
///
/// Providers are typically refreshed at the end of the frame where they
/// notified that they wanted to rebuild.
///
/// Providers are disposed if they spent at least one full frame without any listener.
class _ProviderScheduler {
@internal
class ProviderScheduler {
/// A way to override [vsync], used by Flutter to synchronize a container
/// with the widget tree.
@internal
final flutterVsyncs = <Vsync>{};

/// A function that controls the refresh rate of providers.
///
/// Defaults to refreshing providers at the end of the next event-loop.
@internal
void Function(void Function()) get vsync {
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;
}

final _stateToDispose = <AutoDisposeProviderElementMixin<Object?>>[];
final _stateToRefresh = <ProviderElementBase<Object?>>[];

Expand Down
4 changes: 2 additions & 2 deletions packages/riverpod/test/framework/provider_container_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,7 @@ void main() {

group('.pump', () {
test(
'waits for providers to rebuild or get disposed, no matter from which container they are associated in the graph',
'Waits for providers associated with this container and its parents to rebuild',
() async {
final dep = StateProvider((ref) => 0);
final a = Provider((ref) => ref.watch(dep));
Expand All @@ -417,7 +417,7 @@ void main() {
verifyOnly(bListener, bListener(null, 0));

root.read(dep.notifier).state++;
await root.pump();
await scoped.pump();

verifyOnly(aListener, aListener(0, 1));
verifyOnly(bListener, bListener(0, 1));
Expand Down

0 comments on commit 91acc5c

Please sign in to comment.