riverpod icon indicating copy to clipboard operation
riverpod copied to clipboard

FutureProvider the ref.onCancel() is called but ref.onDispose() is not

Open MxD-js opened this issue 1 year ago • 6 comments

Describe the bug If a I tap on a item from a ListView and pressed the back button the widget is disposed as expected and the provider ref.onRemoveListener is called then ref.onCancel is called also expected but not ref.onDispose is never called and long after the screen is disposed of the video player plays the video in the background and ref.onDispose is never called to dispose the player.

However when I let the the future provider complete and the data is present in the widget page then dispose functions as expected when the user presses the back button.

This issue comes up because when the user clicks on a video item from a ListView and taps back out immediate after the provider is still in a loading state and even though ref.onCancel was called, ref.onDispose is never called to dispose the video controllers and async functions complete and video starts playing on a unmounted and disposed of widget.

(Pardon all the print statements, just trying to figure out what is going on)

To Reproduce

riverpod
// ignore: unsupported_provider_value
FutureOr<ChewieController> videoPlayerItemController(
    VideoPlayerItemControllerRef ref,
    {required String videoId,
    required String timeStamp,
    String? videoIsLive}) async {
  // Youtube explode

  final ytxPlode = YoutubeExplode();
  final VideoPlayerController playerController;
  final ChewieController chewieController;

  ref.onAddListener(() {
    print("Listener Added....");
  });

  ref.onRemoveListener(() {
    print("Listener Removed....");
  });

  ref.onCancel(() {
    print("Canceling....");
  });

  ref.onResume(() {
    print("Resuming....");
  });

  final islive = _checkIfLive(videoIsLive);

  // get videoFromYtExplode:
  final uri = await _getYTMetadata(videoId, ytxPlode, islive);
  // Video Player Controller - Flutter
  playerController = VideoPlayerController.networkUrl(uri);
  await playerController.initialize();

  chewieController = ChewieController(
    autoInitialize: true,
    isLive: islive,
    videoPlayerController: playerController,
    startAt: Duration(seconds: int.parse(timeStamp)),
    autoPlay: true,
  );

  // Clean up
  ref.onDispose(() {
    print("STARTED DISPOSING VIDEO PLAYER!!!!");
    playerController.dispose();
    chewieController.dispose();
    ytxPlode.close();
    print("FINISHED DISPOSING VIDEO PLAYER!!!!");
  });

  return chewieController;
}

and my widget for the video player


class VideoPlayerItem extends HookConsumerWidget {
  const VideoPlayerItem(
      {required this.title,
      required this.id,
      this.isLive,
      this.timeStamp,
      Key? key})
      : super(key: key);
  final String title;
  final String id;
  final String? timeStamp;
  final String? isLive;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final videoController = ref.watch(videoPlayerItemControllerProvider(
        videoId: id,
        timeStamp: timeStamp ?? 0.toString(),
        videoIsLive: isLive));
    final tappedItem = useState<int>(0);

    // Paging Controller using useMemoized to hold the complex object
    // in memory (same as initState for a stateful widget)
    final moreFromThisServicePagingController =
        useMemoized<PagingController<int, ArchiveVideoItem>>(
            () => PagingController(firstPageKey: 0));

    useEffect(() {
      moreFromThisServicePagingController
          .addPageRequestListener((pageKey) => ref.read(
                fetchMoreFromThisServiceControllerProvider(
                    pageKey: pageKey,
                    pagingController: moreFromThisServicePagingController,
                    videoId: id),
              ));
      return () {
       print("USEEFFEECT STARTED DISPOSING WIDGET");
        moreFromThisServicePagingController.dispose();
        print("USEEFFEECT FINISHED DISPOSING WIDGET ");
      };
    }, const []);

    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),

      
      body: videoController.when(
        data: (vController) => Column(
          children: [
            AspectRatio(
                aspectRatio: 16 / 9, child: Chewie(controller: vController)),
            Visibility(
              visible: isLive == "false" ? false : true,
              child: const Padding(
                padding: EdgeInsets.all(10),
                child: Row(children: [
                  Text("more from the service"),
                ]),
              ),
            ),
            Visibility(
              visible: isLive == "false" ? false : true,
              child: Expanded(
                child: PagedListView<int, ArchiveVideoItem>(
                  pagingController: moreFromThisServicePagingController,
                  builderDelegate: PagedChildBuilderDelegate(
                    itemBuilder: (context, item, index) {
                      return Container(
                        decoration: index == tappedItem.value
                            ? BoxDecoration(
                                borderRadius: BorderRadius.circular(5),
                                border: Border.all(
                                    color: Colors.redAccent, width: 1))
                            : null,
                        child: MoreItemListTile(
                            video: item,
                            onPressed: () {
                              tappedItem.value = index;
                              vController.seekTo(Duration(
                                  seconds: int.parse(item.youTubeTimeStamp)));
                            }),
                      );
                    },
                    newPageProgressIndicatorBuilder: (context) =>
                        const VideoListTileShimmer(),
                    firstPageProgressIndicatorBuilder: (context) {
                      return Column(
                        children: List.generate(
                            10, (index) => const VideoListTileShimmer()),
                      );
                    },
                  ),
                ),
              ),
            )
          ],
        ),
        error: (error, stackTrace) => Center(
          child: Text("error occured $error"),
        ),
        loading: () => const Column(
          children: [
            SizedBox(height: 90),
            Center(child: CircularProgressIndicator())
          ],
        ),
      ),
    );
  }
}

Terminal output for when it's working correctly.

flutter: Listener Added.... 
                                          <<<<<-------- at this point the video is playing and correctly working.  
flutter: EXITING VIDEO PLAYER PAGE
flutter: Listener Removed....
flutter: Canceling....
flutter: USEEFFEECT STARTED DISPOSING WIDGET
flutter: USEEFFEECT FINISHED DISPOSING WIDGET
flutter: STARTED DISPOSING VIDEO PLAYER!!!!
flutter: FINISHED DISPOSING VIDEO PLAYER!!!!

but when I quickly access the video player page and then press the back button this is the terminal output.

flutter: Listener Added....
flutter: EXITING VIDEO PLAYER PAGE
flutter: Listener Removed....
flutter: Canceling....
flutter: USEEFFEECT STARTED DISPOSING WIDGET
flutter: USEEFFEECT FINISHED DISPOSING WIDGET

but no ref.onDispose is never called.

Expected behavior Expect that ref.onDispose is called

MxD-js avatar Feb 06 '24 22:02 MxD-js

Please make a minimal reproducible example. I cannot run this code

rrousselGit avatar Feb 07 '24 04:02 rrousselGit

Sure, Here it is. https://github.com/MxD-js/riverpod_repro_issue_3323_example

MxD-js avatar Feb 07 '24 05:02 MxD-js

Hi Guys, I'm following up on this, I've been bashing my head to figure this out.. Any insight into what could be causing this?

MxD-js avatar Feb 10 '24 16:02 MxD-js

I'm working on other stuff at the moment. I have yet to investigate this.

If it's urgent for you, feel free to ask for help on Discord. Otherwise you may have to wait

rrousselGit avatar Feb 10 '24 17:02 rrousselGit

Thanks @rrousselGit I'll reach out on discord.

MxD-js avatar Feb 10 '24 19:02 MxD-js

Does it help to move onDispose before the async/await part?

snapsl avatar Feb 19 '24 14:02 snapsl

Oh sorry I didn't see your answer. Yes, the placement of onDispose is the issue, as you found out yourself.

The documentation has already been updated, so I'll close this

rrousselGit avatar Mar 09 '24 16:03 rrousselGit