How to close StreamProvider with Riverpod in Flutter - flutter

I'm using Riverpod StreamProvider.
And i would like to know 2 things:
1 - I've learned from a youtube video about stream providers and the code the guy in the video coded something like that:
final streamProvider = StreamProvider.autoDispose<int>((ref) {
return Stream.periodic(Duration(seconds: 1), (number) {
if (number < 5)
return number + 1;
else {
return 5;
}
});
});
The question is: from my understanding using a stream method will require me to use "async*", so why there is no need here?
2 - How can i make sure once the stream's number value is equal to 5 the stream provider will close and stop updating the UI?
Thank you so much!

You refer below sample.
final example = StreamProvider.autoDispose((ref) {
final streamController = StreamController<int>();
for(int i=0; i<=5 ; i++){
// read stream values like this might help
streamController.stream.last.then((value) => {if(value==5)
{streamController.close()}});
if(!streamController.isClosed) {
streamController.add(i);
}
}
ref.onDispose(() {
// Closes the StreamController when the state of this provider is destroyed.
streamController.close();
});
return streamController.stream;
});
Refer the document for more info https://riverpod.dev/docs/concepts/providers

Related

How to generate stream in flutter

I am making some experiment with flutter stream. I have a class for generating stream of int. Here is the class :
class CounterRepository {
int _counter = 123;
void increment() {
_counter++;
}
void decrement() {
_counter--;
}
Stream<int> watchCounter() async* {
yield _counter;
}
}
I expect with the change of _counter, watchCounter() will yield updated counter value. When I call increment() or decrement() from UI, it seems the value of _counter is changing but watchCounter doesn't yield the updated _counter value. How to yield updated _counter value here? I am using StreamBuilder from UI to get the streamed data.
You have created your streams using -
Stream<int> watchCounter() async* {
yield _counter;
}
But to reflect the changes of your stream, you need to receive those stream events. You can control those stream events using a StreamController
Creating a stream
Future<void> main() async {
var stream = watchCounter();
}
Using that stream
stream.listen
Subscribes to the stream by calling the listen function and supplys it
with a Function to call back to when there's a new value available.
stream.listen((value) {
print('Value from controller: $value');
});
There are many other approaches to control and manage streams but for your particular question .listen will do the job.
You are missing an infinite while loop, it's a manual stream.
Stream<dynamic> watchCounter() async* {
while (true) {
// you can change it to 5 seconds or higher
await Future.delayed(const Duration(seconds: 1));
yield _counter;
}
}
And after that you just need to call listen:
watchCounter().listen((value) {
// hear can be empty if you want.
});
You can put it in your init state to run, don't put it in your widget build, please.
This should work fine

How to listen to multiple stream subscriptions in a bloc?

I'm trying to create a bloc that depends on two other blocs. For example, I have Bloc C which depends on Bloc A and Bloc B. I'm trying to do something like the following using flutter_bloc in order to achieve it :
class BlocC
extends Bloc< BlocCEvent, BlocCState> {
final BlocA blocA;
final BlocC blocB;
StreamSubscription blocASubscription;
StreamSubscription blocBSubscription;
BlocC({
#required this.blocA,
#required this.blocB,
}) : super((blocA.state is blocALoaded &&
blocB.state is blocBLoaded)
? BlocCLoaded(
blocA: (blocA.state as blocALoaded).arrayFromBlocA,
blocB:
(blocB.state as blocBLoaded).arrayFromBlocB,
)
: BlocCLoading()) {
blocASubscription = blocA.stream.listen((state) {
if (state is blocALoaded) {
add(BlocAUpdated((blocA.state as blocALoaded).arrayFromBlocA));
}
});
blocBSubscription = blocB.stream.listen((state) {
if (state is BlocBLoaded) {
add(BlocBUpdated((blocB.state as BlocBLoaded).arrayFromBlocB));
}
});
}
...
#override
Future<void> close() {
blocASubscription.cancel();
BlocBSubscription.cancel();
return super.close();
}
}
The problem is that I'm getting the following error: Bad state: Stream has already been listened to. I found information about that error in the next post.
I understand the error is happening because a stream can only listen to one bloc at a time, and not to multiple ones. In my case, the stream is already listening to blocA when I try to listen to blocB. However, I'm not sure how to fix this problem.
I will really appreciate any help on this.
You have to merge the two streams into one and act based on the event type:
import 'package:async/async.dart' show StreamGroup;
//...
final blocAStream = blocA.stream;
final blocBStream = blocB.stream;
var blocAandBStreams = StreamGroup.merge([blocAStream, blocBStream]);
blocAandBStream.listen((event){
if(event is BlocAState){
if (event is blocALoaded) { //<-- for readability
add(BlocAUpdated((blocA.state as blocALoaded).arrayFromBlocA));
}
}else if(event is BlocBState){
if (event is BlocBLoaded) {//<-- for readability
add(BlocBUpdated((blocB.state as BlocBLoaded).arrayFromBlocB));
}
}
})
I've implemented event bus pattern to communicate between blocs. The advantage of this approach is that your block instances are not coupled to each other so you don't need to
instantiate or inject their instances Manage Global Events by bloc

Flutter streamProvider not updating

When a user posts a comment. It stores the comment into the database (Mysql) but the streamProvider is not updating the listview for some reason. I'm able to access the provider and the commentData. But when I post a new comment. through add method. As I said, the listview does not display the new comment which has been sent to the database.
class CommentModel {
final int reportId;
final String text;
const CommentModel(this.reportId, this.text);
}
class CommentProvider {
Stream<List<CommentModel>> intStream(int reportId) {
return Stream.fromFuture(getComments(reportId));
}
Future<List<CommentModel>> getComments(int reportId) async {
final comments = await _fetchComments(reportId);
final List<CommentModel> messages = List<CommentModel>();
for (int i = 0; i < comments.length; i++) {
messages.add(CommentModel(reportId, comments[i]["text"])));
}
return messages;
}
Future<void> add(CommentModel data) async {
await _postComment(data.reportId, data.text);
}
}
MultiProvider(
providers: [
ChangeNotifierProvider<CommentProvider>(create: (_) => CommentProvider()),
StreamProvider<List<CommentModel>>(
create: (_) => CommentProvider().intStream(int.tryParse(reportData["id"])),
initialData: null,
),
],
child: CardCommentWidget(
reportId: int.tryParse(reportData["id"]),
),
),
final commentData = Provider.of<List<CommentModel>>(context);
ListView.builder(
key: PageStorageKey("commentsScroll"),
shrinkWrap: true,
itemCount: commentData.length,
itemBuilder: (BuildContext context, int index) {
final comment = commentData[index];
return Text(comment.text);
},
),
Looks like you simply convert the out put of the getComments method to a stream. Meaning when you call the intStream method you will get a stream which only emits the results of the getComments method once. There is nothing letting the stream know that more items are added.
I can't guess what kind of database you are using to back this up and which "streaming" capabilities it has but someone needs to let your stream know that a new item has been added. One way to solve this would be something like this:
Declare a StreamController which will act as stream and sink;
In the intStream method, initialize the StreamController with the outcome of the getComments method and return the stream of the StreamController;
After saving the comment to the database, add the comment to the StreamController.
In code this could look something like this:
class CommentProvider {
final StreamController<List<CommentModel>> _streamController;
Stream<List<CommentModel>> intStream(int reportId) {
// Initialize a new instance of the StreamController
// and emit each comment when someone starts listening
// to the stream.
if (_streamController == null) {
_streamController = StreamController<List<CommentModel>>
.broadcast(
onListen: () async => await getComments(reportId),
onErrror: (error) {
// Handle error here...
},
);
}
return _streamController.stream;
}
Future<List<CommentModel>> getComments(int reportId) async {
final comments = await _fetchComments(reportId);
final List<CommentModel> messages = List<CommentModel>();
for (int i = 0; i < comments.length; i++) {
messages.add(CommentModel(reportId, comments[i]["text"])));
}
return messages;
}
Future<void> add(CommentModel data) async {
await _postComment(data.reportId, data.text);
// Emit the updated list containing the added
// comment on the stream.
if (_streamController != null) {
final comments = await getComments(data.reportId);
_streamController?.add(comments);
}
}
}
This above code is an example and should work. You might need to tweak it a little bit as mentioned in the comments that are part of the code example. And like I mentioned some databases directly support streaming (e.g. Firebase) which directly return the result of a query as a stream and will automatically add items to the stream when they are added to the database and match the query criteria. I couldn't deduce this from your code though.
Some reading material on working with the StreamController class can be found here:
StreamController class;
Using a StreamController
EDIT:
I updated the logic in the add method to make sure the _streamController if not null.
EDIT 2:
Updated the code to return a stream emitting lists of comments, so we can better facilitate the ListView class.

How to turn a Future into a stream for a ListView

I am using this AutoCompleteTextField
https://github.com/sharansingh00002/Auto-Complete-TextField-Flutter
As shown in the example it calls a Future with a URL to pull the data, which populates the dropdown typeahead results.
I need to use the returned data also in another ListView, not just the dropdown of the autocompletetextfield.
My question is how to turn the Future getLocationSuggestionsList into a Stream so I can use a StreamBuilder for the ListView?
Future<List<String>> getLocationSuggestionsList(String locationText) async {
final bloc = BlocProvider.of<EditProfileBloc>(context);
List<String> suggestionList = List();
LocationModel data = await bloc.fetchLocationSuggestions(locationText);
if (data != null) {
for (Predictions predictions in data.predictions) {
suggestionsKeyValuePairs[predictions.description] = predictions.placeId;
if (!suggestionList.contains(predictions.description))
suggestionList.add(predictions.description);
}
return suggestionList;
} else {
return [''];
}
}
You can use FutureBuilder instead of StreamBuilder in that case.

get futures from stream one by one

I have a stream returning values from a web api and a widget that presents one value (using FutureBuilder). The user can iterate through the values with a simple next button. I don't want to load all values in advance but I don't want that each value will be loaded when pressing the next button.
my current code is:
Queue<Item> itemQueue = Queue<Item>();
Future<Item> curItem;
Future<Item> getItem() async {
while (itemQueue.isEmpty)
await Future.delayed(Duration(milliseconds: 250));
return itemQueue.removeFirst();
}
#override
void initState() {
// ...
final sub = stream.listen((item) async {
itemQueue.add(item);
});
// ...
}
#override
Widget build(BuildContext context) {
// ...
ItemWidget(curItem)
// ...
RaisedButton(child: Text("next"),
onPressed: (){
setState(() {
curItem = getItem();
});
},)
// ...
}
this works but feels like it's not the most correct/elegant way of doing this.
is there a better way?
thanks!
EDIT:
As pointed out in the comments, the original answer of creating a broadcast stream may result in events being dropped on the floor. To avoid creating a broadcast stream, we can use StreamIterator to iterate through the stream one element at a time:
final myStream = Stream.fromIterable([1,2,3,4,5]);
// We need to be able to listen to the stream multiple times.
final iter = StreamIterator(myStream);
// The iterator doesn't start at the first element, so we need to
// do that ourselves.
while (await iter.moveNext()) {
// StreamIterator.current will always point to the currently selected
// element of the stream.
print(iter.current);
}
ORIGINAL:
You should be able to utilize Stream.asBroadcastStream and Stream.take to do this.
Your code would look something like this:
final myStream = Stream.fromIterable([1,2,3,4,5]);
// We need to be able to listen to the stream multiple times.
final stream = myStream.asBroadcastStream();
for (int i = 0; i < 5; ++i) {
// Creates a new Stream with 1 element which we can call Stream.first on
// This also changes the contents of stream.
print(await stream.take(1).first);
}
Which would output:
1
2
3
4
5