diff --git a/packages/lite_ref/lib/src/scoped/ref.dart b/packages/lite_ref/lib/src/scoped/ref.dart index 9c8aa53..ae3c50e 100644 --- a/packages/lite_ref/lib/src/scoped/ref.dart +++ b/packages/lite_ref/lib/src/scoped/ref.dart @@ -69,6 +69,9 @@ class ScopedRef { /// 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}); @@ -80,7 +83,7 @@ class ScopedRef { /// } /// } /// ``` - 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.', @@ -90,10 +93,14 @@ class ScopedRef { final existing = element._cache[_id]; - if (existing != null) { - if (autoDispose) { - element._addAutoDisposeBinding(context as Element, existing); + void autoDisposeIfNeeded(ScopedRef ref) { + if (autoDispose && listen) { + element._addAutoDisposeBinding(context as Element, ref); } + } + + if (existing != null) { + autoDisposeIfNeeded(existing); return existing._instance as T; } @@ -102,15 +109,11 @@ class ScopedRef { 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); @@ -119,6 +122,15 @@ class ScopedRef { 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); diff --git a/packages/lite_ref/test/src/scoped/ref_test.dart b/packages/lite_ref/test/src/scoped/ref_test.dart index 5327056..3665d44 100644 --- a/packages/lite_ref/test/src/scoped/ref_test.dart +++ b/packages/lite_ref/test/src/scoped/ref_test.dart @@ -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 = []; + 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 = []; + 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 {