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.
Related
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. ^-^
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);
I need to display a listview in Flutter with data from firestore. Then I want the user to be able to filter the listview by typing his query in a textfield in the appbar. This is the code I came up with for the listview:
_buildAllAds() {
return StreamBuilder(
stream: Firestore.instance.collection("Classificados")
.orderBy('title').snapshots().map((snap) async {
allAds.clear();
snap.documents.forEach((d) {
allAds.add(ClassificadoData(d.documentID,
d.data["title"], d.data["description"], d.data["price"], d.data["images"] ));
});
}),
builder: (context, snapshot) {
// if (!snapshot.hasData) {
// return Center(child: CircularProgressIndicator());
// }
//else{
//}
if (snapshot.hasError) {
print("err:${snapshot.error}");
}
return ListView.builder(
itemCount: allAds.length,
itemBuilder: (context, index) {
ClassificadoData ad = allAds[index];
return ClassificadosTile(ad);
});
});
}
The reason I save the stream data in the List allAds of type ClassificadoData (data items are ads) is because I can then copy it to another List filteredAds on which the user can perform filtering. And the reason I need a stream for allAds is because I want users to be able to see additions/updates in real time.
So this code "works" but it feels a bit awkward and I also can't do nothing with the builder since snaphot remains null all the way (can't show loader during initial data fetch, for example).
Was wondering if there's maybe a more solid way for doing what I want and if it's possible to get a reference to the snapshots down to the builder.
You seem to be mixing two different concepts of using Streams and Stream related Widgets. Ideally you would either use a StreamBuilder and use the data you get from the stream directly on the Widget, or listen to the data and update a variable that is then used to populate your ListView. I've build the latter as an example from your code:
#override
initState(){
_listenToData();
super.initState();
}
_listenToData(){
Firestore.instance.collection("Classificados")
.orderBy('title').snapshots().listen((snap){
allAds.clear();
setState(() {
snap.documents.forEach((d) {
allAds.add(ClassificadoData(d.documentID,
d.data["title"], d.data["description"], d.data["price"], d.data["images"] ));
});
});
});
}
_buildAllAds() {
return ListView.builder(
itemCount: allAds.length,
itemBuilder: (context, index) {
ClassificadoData ad = allAds[index];
return ClassificadosTile(ad);
}
);
}
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();
I'm trying to have a ListView dynamically update depending on the contents of a TextField (a search bar).
The ListView is inside a "ScenariosList" widget, and contains a list of "Scenarios", which is a custom widget containing a title, content and other bits of data (not really relevant but helpful for context). It's content is fetched from a database via a "ScenariosBloc".
The TextField is contained within a "SearchBar" widget.
The goal is to have the contents of the ListView change whenever a change to the TextField is detected.
I'm currently using two individual blocs. ScenariosBloc fetches all the scenarios from the database and FilterScenariosBloc makes the List render a widget to show the scenario if it's title contains the string in the TextView within the SearchBar.
I'm using nested StreamBuilders to do this (see code below).
ScenariosList.dart
// build individual scenario cards
Widget _buildScenarioListItem(Scenario scen, String filter) {
if (!(filter == null || filter == "")) {
print("null filter");
if (!(scen.title.contains(filter))) {
print("match!");
return ScenarioCard(scen);
}
}
return Container();
}
Widget _buildScenarioList(BuildContext context) {
return StreamBuilder(
stream: scenarioBloc.scenarios,
builder: (BuildContext context,
AsyncSnapshot<List<Scenario>> scenariosSnapshot) {
if (!scenariosSnapshot.hasData) {
return CircularProgressIndicator();
}
return StreamBuilder(
stream: filterScenariosBloc.filterQuery,
initialData: "",
builder: (BuildContext context, AsyncSnapshot filterSnapshot) {
if(!filterSnapshot.hasData) return CircularProgressIndicator();
print("Scenarios Snapshot: ${scenariosSnapshot.toString()}");
print("Filter Snapshot: ${filterSnapshot.toString()}");
return ListView.builder(
physics: NeverScrollableScrollPhysics(),
padding: EdgeInsets.all(0),
shrinkWrap: true,
itemCount: scenariosSnapshot.data.length,
itemBuilder: (BuildContext context, int index) {
Scenario scen = scenariosSnapshot.data[index];
return _buildScenarioListItem(scen, filterSnapshot.data);
},
);
});
});
}
}
SearchBar.dart
the onChanged method of the Textfield contains:
// function to filter the scenarios depending on the users input.
void filterSearchResults(String query) {
_filterScenariosBloc.doFilter(query);
}
FilterScenariosBloc.dart
class FilterScenariosBloc {
// stream - only need one listener
final _searchController = StreamController<String>.broadcast();
// output stream
get filterQuery => _searchController.stream;
FilterScenariosBloc() {
doFilter(" ");
}
doFilter(String query) {
_searchController.sink.add(query);
}
}
The user input is sent to the FilterScenariosBloc all fine, but the status of the filterSnapshot is always connectionState.waiting.
Any ideas on how I can resolve this?
I had the same issue. The problem was that my firestore DB rules did not allow read or write for the collection in question. Please see if that solves your prob
I had the same issue, always having the connectionState.waiting, and thus the snapshot.data
was null. This means that for whatever reason, the data could not be fetched.
I then ran the app into the debug mode and got an error like "Cannot fit requested classes into a single dex file". Then I just followed this answer and it solved the issue for me.