Itembuilder doesn't execute in streambuilder, when updating data in firestore - flutter

When I launch the app for the first time it loads in all the items into the animated list. But when If I add an item to firestore, the streambuilder realizes something has happend. BUT it doesn't execute the itembuilder so no new items will show up. However if I relaunches the app, the new items will be loaded. If I would just change a name on an existing item on the firestore database, it will rebuild with no problem.
When I update an item. I have to get the map of tasks and replace it with a new copy that has the item and post/update that to firestore.
I have tried everything I can think off, doesn't get why this happens.
I have printed in the itembuilder and can see that it doesn't execute on an update.
Widget _buildTasksList() {
return new Expanded(
child: new StreamBuilder(
stream: Firestore.instance
.collection("lists")
.document(tasklist.postID)
.snapshots(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (!snapshot.hasData) {
return new Text("Loading");
}
var listdata = snapshot.data;
tasklist.tasks = listdata['tasks'];
tasklist.owners = listdata['owners'];
tasklist.sorter = listdata['sorter'];
return new AnimatedList(
initialItemCount: convertToTaskList(listdata['tasks']).length,
key: _listKey,
itemBuilder: (context, index, animation) {
return new TaskRow(
task: this.listModel[index],
animation: animation,
listModel: this.listModel,
tasklist: tasklist,
onChange: () => _onChange(this.listModel[index]),
);
},
);
},
)
);
}

Got it working by resetting my _listKey, added this before animated list.
_listKey = null;
_listKey = new GlobalKey();

Related

StreamBuilder not refreshing after asyncMap future resolves

I'm using the following StreamBuilder in a Stateful widget:
StreamBuilder<List<int>>(
stream: widget.model.results(widget.type),
builder: (context, snapshot) {
if (!snapshot.hasData) return CircularProgressIndicator();
if (snapshot.hasError) return Text('Error');
final List<int> results = snapshot.data;
return ListView.builder(
itemCount: results.length,
itemBuilder: (context, index) {
return _buildListTile(results[index]);
});
})
And here's the bit where the Streams get built:
// inside the ViewModel
late final List<StreamController> _streamControllers = [
StreamController<List<int>>.broadcast(),
StreamController<List<int>>.broadcast(),
];
List<int> _results = [];
Stream<List<int>> results(int index) =>
_streamControllers[index]
.stream
.debounce(Duration(milliseconds: 500))
.asyncMap((filter) async {
final List<int> assets = await search(filter); // 👈 Future
return _results..addAll(assets);
});
The issue is that the UI doesn't get rebuilt after the search results are returned.
The debugger shows that the Future is getting resolved correctly, but that the UI doesn't get rebuilt once the result is returned (within asyncMap).
Am I using asyncMap correctly? Is there an alternative way to set this up that could potentially get it working?
EDIT: Showing the code that adds events to the stream
[0, 1].forEach((index) =>
textController.addListener(() =>
_streamControllers[index]
.sink
.add(textController[index].text));
U are using asyncMap correctly.
Your issue might be that you add events to stream before Streambuilder starts to listen to widget.model.results(widget.type) stream.
Either use:
BehaviorSubject
final List<BehaviorSubject> _streamControllers = [
BehaviorSubject<List<int>>(),
BehaviorSubject<List<int>>(),
];
or add events AFTER widgets are built (or when we start to listen to them)
How to use onListen callback to start producing events?
You are creating a new Stream every build therefore it will be always empty and won't update correctly. You have the same controller, but asyncMap is creating a new Stream under the hood. The docs:
Creates a new stream with each data event of this stream asynchronously mapped to a new event.
The fix would be to save the instance of the stream after asyncMap is used. This can be done multiple ways. One would be to make a late initialized field inside your State.
late Stream<List<int>> myStream = widget.model.results(widget.type);
and then use this instance in the StreamBuilder:
StreamBuilder<List<int>>(
stream: myStream,
builder: (context, snapshot) {
if (!snapshot.hasData) return CircularProgressIndicator();
if (snapshot.hasError) return Text('Error');
final List<int> results = snapshot.data;
return ListView.builder(
itemCount: results.length,
itemBuilder: (context, index) {
return _buildListTile(results[index]);
});
})
But you can also save the instance in initState or completely outside the widget and make Stream<List<int>> results(int index) return the saved instance or make it the list like this:
List<Stream<List<int>>> results = _streamControllers
.map((s) => s.stream.asyncMap((filter) async {
final List<int> assets = await search(); // 👈 Future
return _results..addAll(assets);
}))
.toList();

How to update the future in FutureBuilder to get new data while using GetX

I am using GetX and FutureBuilder to build a list of Cards from my DB.
Lets say I have 10 products in DB then 10 cards are shown. When I add one more product, the Cards on the HomeScreen aren't updated and I have to Navigate in and Out of page to show the 11th product.
How can I force the update i.e. probably make a "Refresh" button that may load the latest data.
PS: I do not want to use STREAM-BUILDER as I don't wish to listen to all changes actively but only when needed. I also cannot use SetState() as I am using GetX hence no StatefulWidget.
Here is my Card class:
FutureBuilder(
future: databaseController.getData()
builder: (context, snapshot) {
return StaggeredGridView.countBuilder(
itemCount: snapshot.data.length,
crossAxisCount: 2,
itemBuilder: (BuildContext context, int index) =>
GetX<FindDeviceLocation>(builder: (controller) {
return CreateTheCard(
lindex: index,
location: snapshot.data[index]["location"],
summary: snapshot.data[index]["summary"],
description: snapshot.data[index]["description"],
category: snapshot.data[index]["category"],
imageURL: snapshot.data[index]["adImage"],
onTapFunction: () => Get.to(DetailPage(
post: snapshot.data[index],
)));
}),
This is my method that fetches data from DB:
Future getData() async {
QuerySnapshot _firebaseDb = await FirebaseFirestore.instance
.collection("items")
.where("status", isEqualTo: true)
.orderBy("postTime", descending: true)
.get();
return _firebaseDb.docs;
}
The databaseController has a method call update(),so you can call databaseController.update() when you need to update your data.
Use the getx worker
Ever:
If data change you can update the view
ever(index, (value) {
// call your function here
// any code will be called any time index changes
});

Flutter: How to delete item from listview?

DBHelper dbHelper = DBHelper();
List<Map<String, dynamic>> lists;
#override
Widget build(BuildContext context) {
return FutureBuilder<List<Map<String, dynamic>>>(
future: dbHelper.selectMemo(userkey, 1),
builder: (context, snapshot){
if(snapshot.hasData){
if(snapshot.data.length != 0){
lists = List<Map<String, dynamic>>.from(snapshot.data);
return ListView.separated(
separatorBuilder: (context, index){
return Divider(
thickness: 0,
);
},
itemCount: lists.length,
itemBuilder: (context, index){
return ListTile(
title: Text(lists[index]["memo"]),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: (){
setState(() {
lists = List.from(lists)..removeAt(index);
});
},
),
);
},
);
}
}
},
);
}
This is my code. My lists come from sqlflite. And I want to delete my item from Listview. But this code doesn't work. I don't know where I made the mistake.
This behavior is normal. If you print some logs in the build statement, you will find that every time you click the delete button (setState), Widget will build again.
In addition, lists are re-assigned to DB data after each build
lists = List<Map<String, dynamic>>.from(snapshot.data);
So, it looks like the delete operation is not working.
This phenomenon if you've seen Flutter setState part of the source code will be well understood.
In setState, the callback is performed first, and then mark dirty
void setState(VoidCallback fn) {
final dynamic result = fn() as dynamic;
_element.markNeedsBuild();
}
So, there are two ways to solve this problem:
(1) Do not directly change the value of lists, but when the delete button is pressed, to delete the data in the database, so that when Widget build again, the data taken out of the database is correct.
(2) Add a flag to judge whether the data is initialized, and then add a judgment before assigning lists. If the data is initialized, assignment operation will not be carried out
I hope it worked for you. ^-^

Streambuilder with ListView showing same data each tile?

Trying to use StreamBuilder to build out a ListView, but it is showing one item in each row of the ListView. I am using StreamBuilder broadcast. I use add(data).
child: StreamBuilder<GoogleAddress>(
stream: pickupStreamController.stream,
builder: (context, snapshot) => ListView.builder(
itemCount: 8,
itemBuilder: (context, index) {
print('Render $index');
if (snapshot.data != null) {
print("${snapshot.data.address}");
return _createListItem(
icon: Icons.location_on,
googleplace: snapshot.data,
onTap: () {
print("${snapshot.data.address}");
});
} else {}
},
),
),
Here is the StreamController code where I add to the stream. It is adding different GoogleAddress data on each iteration.
GoogleAddress addy = GoogleAddress.fromJsonMap(map);
if (sc != null) {
sc.add(addy);
}
StreamBuilder will use the latest value from the stream. To pass a list to StreamBuilder you would need to pass a list to add.
Which means to grow a list over time, you have to keep a regular list and pass it to add after modifying it.
final addresses = [];
GoogleAddress addy = GoogleAddress.fromJsonMap(map);
addresses.add(addy);
controller.add(addresses);

StreamBuilder not updating ListView.builder after data changed in Flutter

I am new to Flutter and facing an issue with StreamBuilder & ListView.builder.
I am making a network call on click of a button(Apply Button) available in the list of the card, based on that other buttons are displayed.
the issue I am facing is that widgets are not updated after successful network call but, when I refresh the page I am getting updated result.
What I am doing wrong?
I am not Using any State into this. Do I need to use it?
Widget Code
StreamBuilder(
initialData: [],
stream: dataBoc.ListData,
builder: (context, snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemBuilder: (BuildContext context, index) {
return InkWell(
key: Key(snapshot.data[index]["lid"]),
child: DataCard(
DataModel(snapshot.data[index]),
),
onTap: () {
Navigator.pushNamed(context, "/detailPage",
arguments: snapshot.data[index]["id"]);
},
);
},
itemCount: snapshot.data.length,
);
}
},
),
Bloc Code
//Here is how I am adding data to the stream
if (res.statusCode == 200) {
var data = json.decode(res.body);
if (data['status'] == true) {
// listDataStream.sink.add(data['result']);
listDataStream.add(data['result']);
} else {
listDataStream.sink.addError("Failed to Load");
}
}
Expected result: When I make Network call and if it succeeds then based on network result other buttons must be displayed on the appropriate card.
I have fixed this issue. The issue was my widget tree was not well structured and it was breaking the Widget build process.