Skip to content

Commit

Permalink
Add ScopedRef.read(context) (#19)
Browse files Browse the repository at this point in the history
* Add optional listen parameter to of() method and read method in ScopedRef class

* Add tests for reading

* fmt
  • Loading branch information
jinyus authored Mar 19, 2024
1 parent 002967d commit ff2b66e
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 10 deletions.
32 changes: 22 additions & 10 deletions packages/lite_ref/lib/src/scoped/ref.dart
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ class ScopedRef<T> {

/// Returns the instance of [T] in the current scope.
///
/// If [listen] is `false`, theinstance will not be disposed when the widget
/// is unmounted.
///
/// ```dart
/// class SettingsPage extends StatelessWidget {
/// const SettingsPage({super.key});
Expand All @@ -80,7 +83,7 @@ class ScopedRef<T> {
/// }
/// }
/// ```
T of(BuildContext context) {
T of(BuildContext context, {bool listen = true}) {
assert(
context is Element,
'This must be called with the context of a Widget.',
Expand All @@ -90,10 +93,14 @@ class ScopedRef<T> {

final existing = element._cache[_id];

if (existing != null) {
if (autoDispose) {
element._addAutoDisposeBinding(context as Element, existing);
void autoDisposeIfNeeded(ScopedRef<dynamic> ref) {
if (autoDispose && listen) {
element._addAutoDisposeBinding(context as Element, ref);
}
}

if (existing != null) {
autoDisposeIfNeeded(existing);
return existing._instance as T;
}

Expand All @@ -102,15 +109,11 @@ class ScopedRef<T> {
if (refOverride != null) {
refOverride._init(context);
element._cache[_id] = refOverride;
if (autoDispose) {
element._addAutoDisposeBinding(context as Element, refOverride);
}
autoDisposeIfNeeded(refOverride);
return refOverride._instance as T;
}

if (autoDispose) {
element._addAutoDisposeBinding(context as Element, this);
}
autoDisposeIfNeeded(this);

_init(context);

Expand All @@ -119,6 +122,15 @@ class ScopedRef<T> {
return _instance as T;
}

/// Returns the instance of [T] in the current scope without disposing it
/// when the widget is unmounted. This should be used in callbacks like
/// `onPressed` or `onTap`.
///
/// Alias for `of(context, listen: false)`.
T read(BuildContext context) {
return of(context, listen: false);
}

/// Equivalent to calling the [of(context)] getter.
T call(BuildContext context) => of(context);

Expand Down
106 changes: 106 additions & 0 deletions packages/lite_ref/test/src/scoped/ref_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,112 @@ void main() {
expect(find.text('2'), findsOneWidget);
},
);

testWidgets(
'should dispose when all children are unmounted and it is read in parent',
(tester) async {
final disposed = <int>[];
final countRef = Ref.scoped((ctx) => 1, dispose: disposed.add);
final amount = ValueNotifier(3);

await tester.pumpWidget(
MaterialApp(
home: LiteRefScope(
child: Column(
children: [
Builder(
builder: (context) {
return Text('read ${countRef.read(context)}');
},
),
ListenableBuilder(
listenable: amount,
builder: (context, snapshot) {
return Column(
children: [
const SizedBox.shrink(),
for (var i = 0; i < amount.value; i++)
Builder(
builder: (context) {
final val = countRef(context);
expect(val, 1);
return Text('$val');
},
),
],
);
},
),
],
),
),
),
);

await tester.pumpAndSettle();

expect(find.text('1'), findsExactly(amount.value));
expect(find.text('read 1'), findsOneWidget);

expect(disposed, isEmpty);

amount.value = 2;

await tester.pumpAndSettle();

expect(find.text('1'), findsExactly(amount.value));

expect(disposed, isEmpty); // still has listeners

amount.value = 0;

await tester.pumpAndSettle();

expect(find.text('1'), findsNothing);

expect(disposed, [1]); // dispose when all children are unmounted
expect(find.text('read 1'), findsOneWidget);
},
);

testWidgets(
'should NOT dispose when scope is unmounted and only access was a "read"',
(tester) async {
final disposed = <int>[];
final countRef = Ref.scoped((ctx) => 1, dispose: disposed.add);
final show = ValueNotifier(true);
await tester.pumpWidget(
MaterialApp(
home: LiteRefScope(
child: ValueListenableBuilder(
valueListenable: show,
builder: (__, value, _) {
if (!value) return const SizedBox.shrink();
return Builder(
builder: (context) {
final val = countRef.read(context);
expect(val, 1);
return Text('$val');
},
);
},
),
),
),
);

await tester.pumpAndSettle();

expect(find.text('1'), findsOneWidget);
expect(disposed, isEmpty); // overriden instance should be disposed

show.value = false;

await tester.pumpAndSettle();

expect(disposed, isEmpty);
},
);
}

class _Resource implements Disposable {
Expand Down

0 comments on commit ff2b66e

Please sign in to comment.