-
-
Notifications
You must be signed in to change notification settings - Fork 960
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Possible memory leak on StreamProvider.autoDispose #193
Comments
Could you share fully executable examples with step by steps way to reproduce the problem? |
Yes, of course, I have created a github repository with an example with the problem: |
Thanks! I'm able to reproduce the issue. I will investigate |
I think that's a bug related to broadcast streams: void main() {
final stream = Stream.periodic(Duration(seconds: 5), (n) {
print('$n');
return n;
});
final sub = stream.asBroadcastStream().listen((value) {
print('value $value');
});
print('cancel');
sub.cancel();
} This will print |
Interesting case. thanks for the report! Learned something new today TL;DR, the bug was, Riverpod does: StreamSubscription sub;
Stream publicStream;
initState() {
publicStream = stream.asBroadcastStream();
sub = publicStream.listen(...);
}
dispose() {
sub.cancel();
} But with this, while the subscriptions are closed, the broadcast stream never closes The fix appears to be: StreamSubscription sub;
Stream publicStream;
StreamController controller;
initState() {
controller = StreamController.broadcast();
publicStream = controller.stream;
sub = stream.listen(controller.add, onError: controller.addError, onDone: controller.close);
}
dispose() {
sub.cancel();
controller.close();
} |
Hmm but in that case the value printed is because of the stream generator, the case in the example is this: void main() async {
final stream = Stream.periodic(Duration(seconds: 1), (n) {
print('$n');
return n;
});
final sub = stream.asBroadcastStream().map((event) {
print("here");
return event;
}).listen((value) {
print('value $value');
});
print('cancel');
sub.cancel();
await Future.delayed(Duration(seconds: 5));
} You will see the numbers printed, but not "here" message, because the subscription was cancelled. |
Thanks to you for this amazing library. |
Yes but that is not what happen here. The code is not |
Yep, I see, I didn't realize StreamProvider creates internally a broadcast stream. |
Yeah The broadcast stream is needed for |
Oh, I see, so that should be the solution, closing the StreamController in the dispose method. |
Hi @rrousselGit , sorry for comment on this closed issue, I just want to share that I have just realized that the broadcast stream can be closed using asBroadcastStream method too: void main() async {
final stream = Stream.periodic(Duration(seconds: 1), (n) {
print('$n');
return n;
});
final broadcastStream = stream.asBroadcastStream(
onCancel: (sub) {
print("don't have listeners now, so close the underlying subscription");
sub.cancel();
},
);
final sub1 = broadcastStream.listen((value) {
print('value sub1 -> $value');
});
final sub2 = broadcastStream.listen((value) {
print('value sub2 -> $value');
});
print('cancelling sub1');
sub1.cancel();
print('cancelling sub2');
sub2.cancel();
await Future.delayed(Duration(seconds: 5));
} |
Indeed But in this case it makes more sense to have a StreamController |
@rrousselGit version 0.12.0 solves this issue? |
Yes it does |
Hmm I have updated the example to that version, however I am still getting the same problem. Am I missing anything? |
Ah my bad, I fixed it for |
I'll deploy another fix later today, it's a small change |
Perfect, thank you so much 😄 |
Hi @rrousselGit @fpabl0 , final userProvider = StreamProvider((ref) {
return FirebaseAuth.instance.authStateChanges();
});
final firebaseClassData = StreamProvider.autoDispose((ref) {
final userStream = ref.watch(userProvider.last);
final result = userStream.asStream().switchMap((user) {
print('got user $user');
return FirebaseFirestore.instance
.collection('users')
.doc(user.uid)
.collection('courses')
.snapshots()
.map((snapshots) => snapshots.docs);
}).map((event) {
print('snapshot is $event');
return event;
});
print('result is $result');
return result;
});
final classListProvider =
FutureProvider.autoDispose<List<ClassData>>((ref) async {
final prismicClass = await ref.watch(allClassesProvider.future);
print('got prismicClass data $prismicClass');
final userClassData = await ref.watch(firebaseClassData.last);
final result = prismicClass.results
.map((result) => getClassData(result, userClassData))
.toList();
return result;
});
So my code looks like this. For some reason if I see the The logs looks like this
But, if I change (removing |
It is likely that for a few frames, your |
Hi @rrousselGit . Thanks for the quick reply So I tried to reproduce this issue without firebase final testProvider = StreamProvider.autoDispose((ref) async* {
ref.onDispose(() {
print('testProvider disposed');
});
yield '1';
yield '2';
yield '3';
yield '4';
});
final someFutureProvider = FutureProvider((ref) async {
print('future is called');
return 'something';
});
final test2Provider = FutureProvider.autoDispose((ref) async {
final somefuture = await ref.watch(someFutureProvider.future);
print('somefuture is $somefuture');
final a = await ref.watch(testProvider.last);
print('a is $a');
return 'c';
});
class Home extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(body: Center(
child: Consumer(
builder: (context, watch, child) {
final u = watch(test2Provider);
return u.when(
data: (a) => Text(
"anything",
style: TextStyle(color: Colors.white),
),
loading: () => Text(
'a',
style: TextStyle(color: Colors.white),
),
error: (e, s) => Text(
'b',
style: TextStyle(color: Colors.white),
));
},
),
));
}} try to switch final somefuture = await ref.watch(someFutureProvider.future);
print('somefuture is $somefuture');
final a = await ref.watch(testProvider.last);
print('a is $a'); with final a = await ref.watch(testProvider.last);
print('a is $a');
final somefuture = await ref.watch(someFutureProvider.future);
print('somefuture is $somefuture'); what I found is, if I watch futureProvider first, then I'l' got infinite log like this
But if I switch the order, it will be okay here's the log if I watch StreamProvider first
Any clue? is this different issue or same issue? |
It looks like the StreamProvider is dispossed and then recreated |
Btw what I am trying to accomplish here is to combine data from two sources. From firestore (stream) and from an API (future). Am i doing it the right way? |
It may be a bug, I'll look at it later. Can you create a new issue? |
I opened new issue @rrousselGit |
Describe the bug
I am trying to replace StreamBuilders in favor of StreamProvider.autoDispose, however they don't behave equally (don't know if I am misunderstanding StreamProvider, if so, then sorry). Using watch(streamProvider) does not unsubscribe from the stream when the ui is destroyed as a consequence, every time I returned to that screen a new subscription is done with disposing the previous one. That's not the case with the StreamBuilder.
To Reproduce
I have this method in a class that return a stream:
With StreamBuilder (when user goes to other screen, if data changes "here" is not displayed, because the stream subscription has been closed by the streambuilder):
With StreamProvider (when user goes to other screen, "here" message still triggers when data changes, and you can get a lot of them at once, if you returned to the ui that use the stream many times):
Expected behavior
I expect that the subscriptions are totally disposed when the ui that use the streamProvider is destroyed.
The text was updated successfully, but these errors were encountered: