GregoryConrad / rearch-dart

Re-imagined approach to application design and architecture
https://pub.dev/packages/rearch
MIT License
88 stars 4 forks source link

Future completing after RearchConsumer disposal results in `AssertionError` #195

Closed GregoryConrad closed 3 months ago

GregoryConrad commented 3 months ago

Discussed in https://github.com/GregoryConrad/rearch-dart/discussions/194

Originally posted by **busslina** June 5, 2024 I have a Scrollable widget whose children are disposed if they are not visible. One of the children is `_AsyncImage`, a `RearchConsumer` that uses the `future` side effect to precache the image (from the Internet) and show a loading spinner while loading. So the problem came when I scroll quickly enough to get this widget created and disposed before the image get loaded. I got a Stream error which I'm 99% sure it is related to `use.future`. Do you have a solution for this scenario? Widget: ```dart class _AsyncImage extends RearchConsumer { const _AsyncImage(this.image); final Image image; @override Widget build(BuildContext context, WidgetHandle use) { final precacheValue = use.future( use.memo( () => image.precache(context), [image], ), ); switch (precacheValue) { case AsyncLoading(): return const CircularProgressIndicator().centered(); case AsyncError(): return const Label.error('Precaching image'); case AsyncValue(): return image.rounded(radius: 25); } } } ``` Error stack trace (on the web): ``` Error dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/err ors.dart 296:3 throw_ dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/err ors.dart 29:3 assertFailed packages/flutter/src/widgets/framework.dart 5018:49 markNeedsBuild packages/flutter_rearch/src/widgets/consumer.dart 150:5 rebuild packages/rearch/src/side_effects.dart 71:12 setter packages/rearch/src/side_effects.dart 331:29 dart-sdk/lib/async/zone.dart 1594:9 runUnaryGuarded dart-sdk/lib/async/stream_impl.dart 365:5 [_sendData] dart-sdk/lib/async/stream_impl.dart 297:7 [_add] dart-sdk/lib/async/stream_controller.dart 784:5 [_sendData] dart-sdk/lib/async/stream_controller.dart 658:7 [_add] dart-sdk/lib/async/stream.dart 250:17 dart-sdk/lib/async/zone.dart 1661:54 runUnary dart-sdk/lib/async/future_impl.dart 163:18 handleValue dart-sdk/lib/async/future_impl.dart 847:44 handleValueCallback dart-sdk/lib/async/future_impl.dart 876:13 _propagateToListeners dart-sdk/lib/async/future_impl.dart 652:5 [_completeWithValue] dart-sdk/lib/async/future_impl.dart 722:7 callback dart-sdk/lib/async/schedule_microtask.dart 40:11 _microtaskLoop dart-sdk/lib/async/schedule_microtask.dart 49:5 _startMicrotaskLoop dart-sdk/lib/_internal/js_dev_runtime/patch/async_patch.dart 181:7 Error dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/err ors.dart 296:3 throw_ dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/err ors.dart 29:3 assertFailed packages/flutter/src/widgets/framework.dart 5018:49 markNeedsBuild packages/flutter_rearch/src/widgets/consumer.dart 150:5 rebuild packages/rearch/src/side_effects.dart 71:12 setter packages/rearch/src/side_effects.dart 331:29 dart-sdk/lib/async/zone.dart 1594:9 runUnaryGuarded dart-sdk/lib/async/stream_impl.dart 365:5 [_sendData] dart-sdk/lib/async/stream_impl.dart 297:7 [_add] dart-sdk/lib/async/stream_controller.dart 784:5 [_sendData] dart-sdk/lib/async/stream_controller.dart 658:7 [_add] dart-sdk/lib/async/stream.dart 250:17 dart-sdk/lib/async/zone.dart 1661:54 runUnary dart-sdk/lib/async/future_impl.dart 163:18 handleValue dart-sdk/lib/async/future_impl.dart 847:44 handleValueCallback dart-sdk/lib/async/future_impl.dart 876:13 _propagateToListeners dart-sdk/lib/async/future_impl.dart 652:5 [_completeWithValue] dart-sdk/lib/async/future_impl.dart 722:7 callback dart-sdk/lib/async/schedule_microtask.dart 40:11 _microtaskLoop dart-sdk/lib/async/schedule_microtask.dart 49:5 _startMicrotaskLoop dart-sdk/lib/_internal/js_dev_runtime/patch/async_patch.dart 181:7 ```

Reproducible test case demonstrating current issue

  testWidgets("future won't rebuild after build", (tester) async {
    final outerCompleter = Completer<void>();
    final innerCompleter = Completer<void>();
    await tester.pumpWidget(
      MaterialApp(
        home: RearchBootstrapper(
          child: Scaffold(
            body: RearchBuilder(
              builder: (context, use) {
                final val = use.future(outerCompleter.future);
                if (val is AsyncLoading) {
                  return RearchBuilder(
                    builder: (context, use) {
                      use.future(innerCompleter.future);
                      return const CircularProgressIndicator();
                    },
                  );
                } else {
                  return const Text('switched');
                }
              },
            ),
          ),
        ),
      ),
    );
    expect(find.text('switched'), findsNothing);

    outerCompleter.complete(null);
    await tester.pump();
    expect(find.text('switched'), findsOneWidget);

    innerCompleter.complete(null);
    await tester.pump();
    expect(find.text('switched'), findsOneWidget);
  });
GregoryConrad commented 3 months ago

It appears as though this is caused solely by past me implementing use.stream like an absolute idiot and using an imperative approach over a declarative one. Issue appears to be resolved the second I switched it to be more declarative.

busslina commented 3 months ago

I confirm https://github.com/GregoryConrad/rearch-dart/pull/196 fixes the error that I was having in my code. Thanks