Flutter how to get data from async function - asynchronous

I am having extreme difficulty in handling this. I call an async function that will get some info from SQLite but I can't seem to get it. It just renders a empty screen in which should be a listview.
List allItems = new List();
Future<void> pegaDados() async {
var itens = await geraCardapio();
for (var i = 0; i < itens.length; i++) {
print((itens[i].toMap()));
allItems.add(itens[i].toMap());
}
}
print(pegaDados());
return ListView.builder(
itemCount: allItems.length,
itemBuilder: (context, index) {
return ListTile(
leading: Image.asset("assets/"+ allItems[index]['imagem'], fit: BoxFit.contain,),
title: Text(allItems[index]['pedido']),
trailing: Text(allItems[index]['valor']),
);
},
);
Thank you very much.
I managed to get the solution, thanks to both people who answered the question (using both solutions I managed to get this little frankenstein)
Future<dynamic> pegaDados() async {
var allItems = await geraCardapio();
return allItems.map((allItems) => allItems.toMap());
}
return FutureBuilder(
future: pegaDados(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
print(snapshot.data);
var objeto = [];
for (var i in snapshot.data) {
objeto.add(i);
}
print(objeto);
return Container(
child: ListView.builder(
itemCount: objeto.length,
itemBuilder: (context, index) {
return ListTile(
leading: Image.asset("assets/"+ objeto[index]['imagem'], fit: BoxFit.contain),
title: Text(objeto[index]['pedido']),
trailing: Text(objeto[index]['valor'].toString()),
);
},
),
);
} else if (snapshot.hasError) {
throw snapshot.error;
} else {
return Center(child: CircularProgressIndicator());
}
});
thanks to [Mohammad Assem Nasser][1] and [Eliya Cohen][2] for the help!
[1]: https://stackoverflow.com/users/11542171/mohammad-assem-nasser
[2]: https://stackoverflow.com/users/1860540/eliya-cohen

You should first understand what is Future operations (Future function in your case). Future operations are the operations which take time to perform and return the result later. To handle this problem, we use Asynchronous functions.
Asynchronous Function let your program continue other operations while the current operation is being performed. Dart uses Future objects (Futures) to represent the results of asynchronous operations. To handle these operations, we can use async/await, but it is not possible to integrate async and await on widgets. So it is quite tricky to handle futures in widgets. To solve this problem flutter provided a widget called FutureBuilder.
In FutureBuilder, it calls the Future function to wait for the result, and as soon as it produces the result it calls the builder function where we build the widget.
Here is how it should be:
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
List allItems = new List();
Future<List> pegaDados() async{
var items = await geraCardapio(); // TODO: Add this function to this class
for (var i = 0; i < items.length; i++) {
print((items[i].toMap()));
allItems.add(items[i].toMap());
}
return items;
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(title: Text('Demo')),
body: FutureBuilder(
future: pegaDados(),
builder: (context, snapshot){
if(snapshot.connectionState == ConnectionState.done){
return Container(
child: ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, index) {
return ListTile(
leading: Image.asset("assets/"+ snapshot.data[index]['imagem'], fit: BoxFit.contain,),
title: Text(snapshot.data[index]['pedido']),
trailing: Text(snapshot.data[index]['valor']),
);
},
),
);
}
else if(snapshot.hasError){
throw snapshot.error;
}
else{
return Center(child: CircularProgressIndicator());
}
},
),
);
}
}
Here is the link to a short video that will explain FutureBuilder in a concise way.

I'm not sure how your widget tree looks like, but I'm assuming ListView is being built simultaneously with pegaDados. What you're looking for is a FutureBuilder:
Future<dynamic> pegaDados() async{
var items = await geraCardapio();
return items.map((item) => item.toMap());
}
...
FutureBuilder<dynamic>(
future: pegaDados(),
builder: (BuilderContext context, AsyncSnapshot snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
return Text('Uninitialized');
case ConnectionState.active:
case ConnectionState.waiting:
return Text('Awaiting result...');
case ConnectionState.done:
if (snapshot.hasError)
throw snapshot.error;
//
// Place here your ListView.
//
}
return null; // unreachable
}

Related

Bad state: Snapshot has neither data nor error in flutter when using StreamBuilder and Firestore

I'm adding data from Firestore to a Stream from StreamBuilder, but I'm getting the following error:
Exception has occurred. StateError (Bad state: Snapshot has neither data nor error
My code.
class Home extends StatefulWidget {
const Home({Key? key}) : super(key: key);
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
AppState? estado;
static String? userID = FirebaseAuth.instance.currentUser?.uid;
static final userColeccion = FirebaseFirestore.instance.collection("users");
var groupfav = ' ';
Stream<QuerySnapshot>? taskGroup;
#override
void initState() {
super.initState();
getGroupFavData();
}
void getGroupFavData() async {
var groupFavData = await userColeccion.doc("$userID").get();
var groupfav = groupFavData.data()!['groupfav'];
taskGroup = FirebaseFirestore.instance
.collection("groups")
.doc(groupfav) // pass the obtained value
.collection("task")
.snapshots();
}
#override
Widget build(BuildContext context) {
estado = Provider.of<AppState>(context, listen: true);
return Scaffold(
appBar: AppBar(
title: const Text("Home"),
automaticallyImplyLeading: false,
),
body: StreamBuilder(
stream: taskGroup,
builder: (
BuildContext context,
AsyncSnapshot<QuerySnapshot> snapshot,
) {
if (snapshot.hasError) {
return const Text("error");
}
if (snapshot.connectionState == ConnectionState.waiting) {
return const Text("Loading");
}
var data = snapshot.requireData;
return ListView.builder(
itemCount: data.size,
itemBuilder: (context, index) {
return Card(
child: ListTile(
title: Text("${data.docs[index]['titulo']}"),
subtitle: Text("${data.docs[index]['contenido']}"),
onTap: () {},
trailing: IconButton(
icon: const Icon(Icons.delete),
color: Colors.red[200],
onPressed: () {},
),
),
);
},
);
},
),
);
}
}
Ok, looking at your issue, I see that 1) you need to get the data of the document BEFORE you start listening on that document, which is normal, so you want to do a call first to the collection, get the document, then listen on the document's collection called task, which makes sense. Your issue is still an asynchronous issue. The app is rebuilding on a stream that still hasn't arrived; you have to fix the sequence of things.
You then need to switch things up a bit and do the following:
Option #1:
a) Use a FutureBuilder: this will allow you to make the async call to get the document name based on the user Id
b) After you get the document associated to that user, you want to listen on the stream produced by the collection called tasks in that document. There is where then you can hook up the StreamBuilder.
Option #2:
a) Keep things the way you have, but do a listen on the taskGroup snapshots; but keep rebuilding the list as the values arrive on that collection.
Those are my suggestions.
Here's some brief code on option 1:
// .. in your Scaffold's body:
Scaffold(
body: FutureBuilder( // the future builder fetches the initial data
future: userColeccion.doc("$userID").get(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasData) {
var groupfav = snapshot.data()!['groupfav'];
// then once the 'groupfav' has arrived,
// start listening on the taskGroup
taskGroup = FirebaseFirestore.instance
.collection("groups")
.doc(groupfav) // pass the obtained value
.collection("task")
.snapshots();
return StreamBuilder(
stream: taskGroup,
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
// the rest of your code
});
}
return CircularProgressIndicator();
}
)
)
Option 2 would be something like:
List<Task> userTasks = [];
void getGroupFavData() async {
var groupFavData = await userColeccion.doc("$userID").get();
var groupfav = groupFavData.data()!['groupfav'];
taskGroup = FirebaseFirestore.instance
.collection("groups")
.doc(groupfav) // pass the obtained value
.collection("task")
.snapshots().listen((snapshot) {
// here populate a list of your tasks
// and trigger a widget rebuild once you've grabbed the values
// and display it as a list on the UI
setState(() {
userTasks = snapshot.docs.map((d) => Task.fromJson(d.data())).toList();
});
});
}
And in your Scaffold, you can have a ListView just rendering the items on that task list, like:
ListView.builder(
itemCount: userTasks.length,
itemBuilder: (context, index) {
// render your tasks here
})
Here's a Gist with some working code to illustrate my point. Run it on DartPad and you'll see how using a FutureBuilder wrapping a StreamBuilder will accomplish what you want.
If you run the above code on DartPad, you'll get the following output:
Hope those pointers take you somewhere.

StreamBuilder in Flutter stuck with ConnectionState.waiting and displays only the loading mark

Hi I am trying to display the data inside the Firebase documents into my Flutter dynamically where they get rendered using a loop, so I made a List<Widget> Cards and added to it the function makeItem() that contains the cards, and put them inside a loop, so the problem is that when I run the code it outputs print(snapshot.connectionState); as ConnectionState.waiting all the time and it should be async snapshot yet it refuses to load the data as required, I should mention that the data is display as wanted when I hit "Hot reload in Android Studio" .
so I don't know how resolve this issue. Thanks in Advance
I had the same problem when using STREAM BUILDER with PROVIDER&CHANGE NOTIFIER.
When returning back to the view, one should re-assign the stream itself.
Make a get function for your stream and in that function before returning your stream re-assign the stream. That solved the problem of loading issue for me.
Can you try the following?
class MyList extends StatelessWidget {
#override
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: _firestore.collection(widget.city).snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError)
return Text('Error: ${snapshot.error}');
switch (snapshot.connectionState) {
case ConnectionState.waiting: return Center(child: CircularProgressIndicator(backgroundColor: Colors.amber,strokeWidth: 1),),
default:
return ListView(
children: snapshot.data.documents.map((DocumentSnapshot document) {
return makeItem(
pointName: document['name'],
huge: document['lastname'],
moderate: document['mobileNumber'],
none: document['location'],
fights: document['job'],
);
}).toList(),
);
}
},
);
}
}
I think I got something for you try this out. It works on my emulator.
List<Widget> cards = [];
Stream<QuerySnapshot> firebaseStream;
#override
void initState() {
super.initState();
firebaseStream = Firestore.instance.collection('Hearings').snapshots();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: StreamBuilder<QuerySnapshot>(
stream: firebaseStream,
builder: (BuildContext context,
AsyncSnapshot<QuerySnapshot> asyncSnapshot) {
List<DocumentSnapshot> snapData;
if (asyncSnapshot.connectionState == ConnectionState.waiting) {
return Container(
child: Center(
child: CircularProgressIndicator(
backgroundColor: Colors.amber,
strokeWidth: 1,
),
),
);
} else if (asyncSnapshot.connectionState ==
ConnectionState.active) {
snapData = asyncSnapshot.data.documents;
if (asyncSnapshot.hasData) {
for (int i = 0; i < snapData.length; i++) {
Widget card = Text(snapData[i].data['locationName']);
cards.add(card);
}
}
}
return ListView.builder(
itemCount: cards.length,
itemBuilder: (context, index) => cards[index],
);
},
),
),
);
I got bad news too though now that the data is updating it exposed some flaws in your logic its duplicating old entries in your array. You'll see. That should be easy to fix though.
Using stream.cast() on StreamBuilder solved my problem.

Flutter: Correct approach to get value from Future

I have a function which returns images directory path, it performs some additional check like if directory exists or not, then it behaves accordingly.
Here is my code:
Future<String> getImagesPath() async {
final Directory appDir = await getApplicationDocumentsDirectory();
final String appDirPath = appDir.path;
final String imgPath = appDirPath + '/data/images';
final imgDir = new Directory(imgPath);
bool dirExists = await imgDir.exists();
if (!dirExists) {
await new Directory(imgPath).create(recursive: true);
}
return imgPath;
}
This piece of code works as expected, but I'm having issue in getting value from Future.
Case Scenario:
I have data stored in local database and trying to display it, inside listview. I'm using FutureBuilder, as explained in this answer. Each data row has an image connected with it (connected means, the image name is stored in db).
Inside Widget build method, I have this code:
#override
Widget build(BuildContext context) {
getImagesPath().then((path){
imagesPath = path;
print(imagesPath); //prints correct path
});
print(imagesPath); //prints null
return Scaffold(
//removed
body: FutureBuilder<List>(
future: databaseHelper.getList(),
initialData: List(),
builder: (context, snapshot) {
return snapshot.hasData
? ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (_, int position) {
final item = snapshot.data[position];
final image = "$imagesPath/${item.row[0]}.jpg";
return Card(
child: ListTile(
leading: Image.asset(image),
title: Text(item.row[1]),
subtitle: Text(item.row[2]),
trailing: Icon(Icons.launch),
));
})
: Center(
child: CircularProgressIndicator(),
);
}));
}
Shifting return Scaffold(.....) inside .then doesn't work. Because widget build returns nothing.
The other option I found is async/await but at the end, same problem, code available below:
_getImagesPath() async {
return await imgPath();
}
Calling _getImagesPath() returns Future, instead of actual data.
I beleive there is very small logical mistake, but unable to find it myself.
I see that you have to build your widget from the output of two futures. You can either use two FutureBuilders or have a helper method to combine them into one simplified code unit.
Also, never compute/invoke async function from build function. It has to be initialized before (either in constructor or initState method), otherwise the widget might end up repainting itself forever.
Coming to the solution: to simplify code, it is better to combine both future outputs into a single class as in the example below:
Data required for build method:
class DataRequiredForBuild {
String imagesPath;
List items;
DataRequiredForBuild({
this.imagesPath,
this.items,
});
}
Function to fetch all required data:
Future<DataRequiredForBuild> _fetchAllData() async {
return DataRequiredForBuild(
imagesPath: await getImagesPath(),
items: await databaseHelperGetList(),
);
}
Now putting everything together in Widget:
Future<DataRequiredForBuild> _dataRequiredForBuild;
#override
void initState() {
super.initState();
// this should not be done in build method.
_dataRequiredForBuild = _fetchAllData();
}
#override
Widget build(BuildContext context) {
return Scaffold(
//removed
body: FutureBuilder<DataRequiredForBuild>(
future: _dataRequiredForBuild,
builder: (context, snapshot) {
return snapshot.hasData
? ListView.builder(
itemCount: snapshot.data.items.length,
itemBuilder: (_, int position) {
final item = snapshot.data.items[position];
final image = "${snapshot.data.imagesPath}/${item.row[0]}.jpg";
return Card(
child: ListTile(
leading: Image.asset(image),
title: Text(item.row[1]),
subtitle: Text(item.row[2]),
trailing: Icon(Icons.launch),
));
})
: Center(
child: CircularProgressIndicator(),
);
},
),
);
}
Hope it helps.
Moving this piece of code inside FutureBuilder should resolve the issue.
getImagesPath().then((path){
imagesPath = path;
print(imagesPath); //prints correct path
});
So your final code should look like this:
#override
Widget build(BuildContext context) {
return Scaffold(
//removed
body: FutureBuilder<List>(
future: databaseHelper.getList(),
initialData: List(),
builder: (context, snapshot) {
return snapshot.hasData
? ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (_, int position) {
getImagesPath().then((path){
imagesPath = path;
});
final item = snapshot.data[position];
final image = "$imagesPath/${item.row[0]}.jpg";
return Card(
child: ListTile(
leading: Image.file(File(image)),
title: Text(item.row[1]),
subtitle: Text(item.row[2]),
trailing: Icon(Icons.launch),
));
})
: Center(
child: CircularProgressIndicator(),
);
}));
}
Hope it helps!

Implementing pull Down to Referesh

How would you implement a pull-down to refresh in Flutter app that gets data from a collection in Firestore preferable using a StreamBuilder or a FutureBuilder and displays it in a ListView ?
I ended up using information from here. How to refresh or reload a flutter firestore streambuilder manually?
I added the Refresh indicator and made the my stream get its data from a function see code below.
var stream;
#override
void initState() {
setState(() {
stream = mouvesStream();
});
super.initState();
}
Stream<QuerySnapshot> stream() {
return Firestore.instance.collection(_locationState).snapshots();
}
#override
Widget build(BuildContext context) {
body: StreamBuilder(
stream: stream,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasError) {
return Text(snapshot.error);
}
if (snapshot.connectionState == ConnectionState.active) {
List aList = new List();
aList.clear();
for (DocumentSnapshot _doc in snapshot.data.documents) {
Model _add = new Model.from(_doc);
aList.add(_add);
}
return TabBarView(
children: <Widget>[
RefreshIndicator(
onRefresh: _handleRefresh,
child: ListView.builder(
itemCount: aList.length,
itemBuilder: (context, index) {
return Card(aList[index]);
},
),
),
Icon(Icons.directions_transit),
],
);
} else {
return Container(
child: Center(child: CircularProgressIndicator()));
}
})));
}

Can't get Flutter to create multiple widget based on Firebase data

I have tried for a week, searching and changing my code but I just cannot get it to work..
I want to create a list of widget inside a stack and have tried many methods. The following is my test code using forEach method and StreamBuilder methods:
Method 1:
class Recordlist extends StatelessWidget {
#override
Widget build(BuildContext context) {
Firestore.instance.collection('records').document(uid).collection("pdrecords").getDocuments().then( (querySS) {
querySS.documents.forEach( (doc) {
return new Container();
});
}).catchError( (e) {
return new Container();
});
}
}
The above complains that the function never returns a widget
Method 2:
class Recordlist extends StatelessWidget {
#override
Widget build(BuildContext context) {
Double count = 0.5;
return new StreamBuilder <QuerySnapshot>(
stream: Firestore.instance.collection('records').document(uid).collection("pdrecords").snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
//final int recordCount = snapshot.data.documents.length;
if (snapshot.hasData) {
final int recordCount = snapshot.data.documents.length;
print("got data. records: $recordCount");
if (recordCount >0) {
switch (snapshot.connectionState) {
case ConnectionState.none: return new Text('');
case ConnectionState.waiting: return new Text('Fetching data ...');
case ConnectionState.active: {
count = count + 1.0; print("count: $count");
return new ListPDRecord(
margin: new EdgeInsets.only(bottom: 10.0) * count, // listSlidePosition * 5.5,
width: listTileWidth.value,
title: snapshot.data.documents.elementAt(0)['starttime'],
subtitle: snapshot.data.documents.elementAt(0)['endtime'],
image: nodata,
);
}
break;
case ConnectionState.done: return new Text('Done:');
}
} else {
return noDataListData(
listSlidePosition: listSlidePosition,
listTileWidth: listTileWidth,
);
}
}
}
This method always show 1 item no matter how many items I have in the Firebase database. I suspect it is returning the same position so it has many items stacked on top of each other.
I am getting really confused. Really appreciate some help.
You are building only one time. You have to take each document in the snapshot and map it to a widget. Firebase way of doing it is below.
StreamBuilder(
stream: Firestore.instance
.collection('flutter-posts')
.orderBy('postTime', descending: true)
.snapshots(),
builder:
(BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (!snapshot.hasData)
return const Center(child: CircularProgressIndicator());
return ListView(
padding: const EdgeInsets.only(top: 8.0),
children:
snapshot.data.documents.map((DocumentSnapshot document) {
return SinglePost(
document: document,
currentUID: appState.firebaseUser.uid,
);
}).toList(),
);
},
),

Resources