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
Related
I'm using flutter and firebase realtime database.I'm trying to read data from a specific node.I'm saving the data that I am collecting in the Orderlist class and then I return a Future List of Ordelist.This Future function I am trying to use on another widget.I want to display on screen every time data is updated.
Future<List<Orderlist>> order() async{
String business =await businessname();
List table = await tables();
List<Orderlist> list = [];
table.forEach((element) async{
String payment_method = '';
String payment_state ='';
var snapshot = ref.child(business).child(element.toString()).onValue.listen((event) {
event.snapshot.children.forEach((method) {
if(method.key=='payment_method') payment_method=method.value.toString();
if(method.key=='payment_state') payment_state = method.value.toString();
});
final order = Orderlist(payment_method: payment_method,payment_state: payment_state);
list.add(order);
});
});
return list;
}
The problem is that at first place the data are loaded on screen but when I am trying to update the data for some reason the list is appended whereas I just want to replace the previous data with the updated data.To be more specific if I want to listen to 2 nodes to be updated I will have a list with 2 Orderlist items.But the problem is when I update one of them the list is expanded to 3 Orderlist items.
Here is the widget where I am trying to use the Future function
first data loaded Updated da
class TempSettings extends StatefulWidget {
const TempSettings({super.key});
#override
State<TempSettings> createState() => _TempSettingsState();
}
class _TempSettingsState extends State<TempSettings> {
String? business;
List<Orderlist> list=[];
final user = FirebaseAuth.instance.currentUser;
#override
void initState() {
// TODO: implement initState
g();
super.initState();
}
void g() async{
list = await DatabaseManager(user_uid: user!.uid).order();[![[![enter image description here](https://i.stack.imgur.com/hn2NQ.png)](https://i.stack.imgur.com/hn2NQ.png)](https://i.stack.imgur.com/SJ4M1.png)](https://i.stack.imgur.com/SJ4M1.png)
}
#override
Widget build(BuildContext context) {
return Column(children: list.map((e) => ListTile(title: Text(e.payment_method!),)).toList(),);
}
}
When you listen to data in Firebase with onValue, the event you get contains a snapshot of all data. Even when only one child node was added/changed/removed, the snapshot will contain all data. So when you add the data from the snapshot to list, you are initially adding all child nodes once - but then on an update, you're adding most of them again.
The easiest way to fix this is to empty the list every time onValue fires an event by calling list.clear().
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.
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
I'm working on ListView filtering like this.
Le't say you type tsi. Then the result is:
which is correct. And if you clear the search input box, the default/original list should be displayed. Instead, there will be duplicated items. Weird.
Here's the filtering code:
onSearchTextChanged(String input) async {
List<ParkingItem> dummySearchList = List<ParkingItem>();
dummySearchList.addAll(_parkingList);
if (input.isNotEmpty){
List<ParkingItem> dummy = List<ParkingItem>();
dummySearchList.forEach((item){
if (item.location.toLowerCase().contains(input.toLowerCase())){
dummy.add(item);
}
});
setState((){
_parkingList.clear();
_parkingList.addAll(dummy);
});
return;
}
else {
setState(() {
_parkingList.clear();
_parkingList.addAll(backup);
});
}
}
And this is the full code. What's wrong here?
Using that dummySearchList and the other dummy list, seems a bit confusing to me. I would suggest having two lists in the state of your widget. One containing all the items, basically the source of your ParkingItems, and one for the items you want to display.
I've typed this out a bit in dartpad, hopefully this might help you.
List<ParkingItem> _allItems;
List<ParkingItem> _displayedItems;
#override
initState() {
super.initState();
_allItems = []; // TODO: fetch all items first
_displayedItems = _allItems; // initially, display all items
}
onSearchTextChanged(String input) async {
if(input.isNotEmpty) {
setState(() {
// set displayed items as a filtered set of allItems, by using `where`
_displayedItems = _allItems
.where((ParkingItem item) => item.location.toLowerCase().contains(input.toLowerCase()))
.toList(); // turn the Iterable back into a List
});
} else {
setState(() {
// no search field input, display all items
_displayedItems = _allItems;
});
}
}
Is it possible to execute a event when the text of a TextField is changed but after a pause.
Suppose, I have a search box but I don't want to change the search data after the user enters each letter but instead the search should take place only if the user entered and paused for a while.
Gunter is correct about using a debounce function, but the one in RxDart only works for Observables (as he pointed out you can convert the onChanged events into a stream and go that route). You can also easily implement your own to accept any function.
// Define this function somewhere
import 'dart:async';
// This map will track all your pending function calls
Map<Function, Timer> _timeouts = {};
void debounce(Duration timeout, Function target, [List arguments = const []]) {
if (_timeouts.containsKey(target)) {
_timeouts[target].cancel();
}
Timer timer = Timer(timeout, () {
Function.apply(target, arguments);
});
_timeouts[target] = timer;
}
Then, you can use it like so in your widget
void _onChanged(String val) {
// ...
}
Widget build(BuildContext context) {
// ...
TextField(
// ...
onChanged: (val) => debounce(const Duration(milliseconds: 300), _onChanged, [val]),
)
// ...
}
You probably want something like debounce provided by https://pub.dartlang.org/documentation/rxdart/latest/rx/Observable/debounce.html
new Observable.range(1, 100)
.debounce(new Duration(seconds: 1))
.listen(print); // prints 100