I am trying to display a list of cartitems in my app with a future builder. 'Awaiting result...' is displayed for maybe 0.5 sec and then only a white screen is shown. When I replace the Listview with a text, the text is shown like intended. So it has something to do with the snapshot I guess...
Widget buildList() {
return new FutureBuilder<List<CartItem>> (
future: getCartItems(),
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
return new Text('Press button to start');
case ConnectionState.waiting:
return new Text('Awaiting result...');
default:
print(snapshot.data);
print(snapshot.hasData);
return
(!snapshot.hasData)
?
new Container(
alignment: FractionalOffset.center,
child: new CircularProgressIndicator())
:
new ListView(
children: snapshot.data,
);
}});
}}
This is my getCartItems:
Future<List<CartItem>> getCartItems() async {
final FirebaseUser user = await FirebaseAuth.instance.currentUser();
final uid = user.uid;
List<CartItem> cartItems = [];
QuerySnapshot data = await Firestore.instance
.collection("carts")
.where('owner', isEqualTo: uid)
.where('active', isEqualTo: true)
.getDocuments();
data.documents.forEach((DocumentSnapshot doc) async {
var keys = doc["products"].keys.toList();
var values = doc["products"].values.toList();
for (var i = 0; i < keys.length; i++){
await Firestore.instance.collection('products').document(keys[i]).get().then((DocumentSnapshot ds) {
cartItems.add( new CartItem.fromDocument(ds, values[i]));
print(cartItems);
});
}
});
return cartItems;
}
I know that the Futurebuilder is called twice:
https://github.com/flutter/flutter/issues/18490
But I don't know how to handle this situation...
Any ideas? Do I really need a FutureBuilder in this scenario?
EDIT:
I added some prints in the buildList Widget with following output:
Performing hot reload...
Reloaded 5 of 710 libraries in 808ms.
I/flutter (23685): [] <--- snapshit.data
I/flutter (23685): true <--- snapshot.hasData
EDIT 2:
Changes, but still not working:
Future<List<CartItem>> getCartItems() async {
final FirebaseUser user = await FirebaseAuth.instance.currentUser();
final uid = user.uid;
List<CartItem> cartItems = [];
QuerySnapshot data = await Firestore.instance
.collection("carts")
.where('owner', isEqualTo: uid)
.where('active', isEqualTo: true)
.getDocuments();
cartItems = await _fetchDocumentData(data);
return cartItems;
}
Future<List<CartItem>> _fetchDocumentData(data) async {
List<CartItem> cartItems = [];
data.documents.forEach((DocumentSnapshot doc) {
var keys = doc["products"].keys.toList();
var values = doc["products"].values.toList();
for (var i = 0; i < keys.length; i++){
Firestore.instance.collection('products').document(keys[i]).get().then((DocumentSnapshot ds) {
cartItems.add( new CartItem.fromDocument(ds, values[i]));
print(cartItems);
});
}
});
return cartItems;
}
EDIT 3:
The output in the console looks like the Futurebuilder is executed before the fetch is completed:
I/flutter ( 4535): [] <--- print(snapshot.data) in FutureBuilder
I/flutter ( 4535): true <--- print(snapshot.hasData) in FutureBuilder
I/flutter ( 4535): [CartItem] <--- print(cartItems) in _fetchDocumentData
The problem may be here :
new ListView(
children: snapshot.data,
);
The parameter children takes List<Widget> as parameter. You are providing data directly to it as parameter and so it is not showing any results on UI to you.
You can use ListView.builder to display your data like this:
ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, position) {
var cartItem = snapshot.data[position];
return Row(
children: <Widget>[
Expanded(child: Text(cartItem.id)), // just for example
Expanded(child: Text(cartItem.name)), // just for example
Expanded(child: Text(cartItem.color)), // just for example
],
);
},
)
Edit:
put code to fetch data in a method like _fetchDocumentData and declare that method async and have return type of Future<List<CartItem>> like this:
Future<List<CartItem>> _fetchDocumentData async {
List<CartItem> cartItems = [];
data.documents.forEach((DocumentSnapshot doc) {
var keys = doc["products"].keys.toList();
var values = doc["products"].values.toList();
for (var i = 0; i < keys.length; i++){
await Firestore.instance.collection('products').document(keys[i]).get().then((DocumentSnapshot ds) {
cartItems.add( new CartItem.fromDocument(ds, values[i]));
print(cartItems);
});
}
});
return cartItems;
}
and before returning cartItems fetch cartItems from _fetchDocumentData like this
cartItems = await _fetchDocumentData();
Explaination:
Your loop is async and so the values are returned prior to completion of loop's execution.
Edit: As you asked here is the code changes that you might want to make.
Change this:
Future<List<CartItem>> getCartItems() async {
final FirebaseUser user = await FirebaseAuth.instance.currentUser();
final uid = user.uid;
List<CartItem> cartItems = [];
QuerySnapshot data = await Firestore.instance
.collection("carts")
.where('owner', isEqualTo: uid)
.where('active', isEqualTo: true)
.getDocuments();
data.documents.forEach((DocumentSnapshot doc) async {
var keys = doc["products"].keys.toList();
var values = doc["products"].values.toList();
for (var i = 0; i < keys.length; i++){
await Firestore.instance.collection('products').document(keys[i]).get().then((DocumentSnapshot ds) {
cartItems.add( new CartItem.fromDocument(ds, values[i]));
print(cartItems);
});
}
});
return cartItems;
}
To this:
Future<List<CartItem>> getCartItems() async {
final FirebaseUser user = await FirebaseAuth.instance.currentUser();
final uid = user.uid;
List<CartItem> cartItems = [];
QuerySnapshot data = await Firestore.instance
.collection("carts")
.where('owner', isEqualTo: uid)
.where('active', isEqualTo: true)
.getDocuments();
cartItems = await _fetchDocumentData();
return cartItems;
}
Future<List<CartItem>> _fetchDocumentData async {
List<CartItem> cartItems = [];
data.documents.forEach((DocumentSnapshot doc) {
var keys = doc["products"].keys.toList();
var values = doc["products"].values.toList();
for (var i = 0; i < keys.length; i++){
await Firestore.instance.collection('products').document(keys[i]).get().then((DocumentSnapshot ds) {
cartItems.add( new CartItem.fromDocument(ds, values[i]));
print(cartItems);
});
}
});
return cartItems;
}
Related
Before this problem happen I was dealing with Future handling to return the values I saved on a sharedPreference, however my _deleteTodo method was working just fine.
After I added the FutureBuilder and finally got my values rendered on the UI now I'm struggling with this new bug.
Every time I update the state of and item my UI reflects it but immediately undoes it
I tried to see if it was something with my _deleteTodo method so I changed the setState to only change the boolean from false to true, but it does exactly the same.
I also print the length of my List after the _deleteTodo and something funny happens: it works one time, the _deleteTodo erase the Todo but after that it doesn't work anymore
This is my TODO class
class Todo {
Todo ({this.title,this.isDone = false});
String title;
bool isDone;
//Decode method to convert a Json String into a Dynamic object
Todo.fromJson(Map <String, dynamic> json)
: title = json ["title"],
isDone = json ["isDone"];
Map <String,dynamic> toJson() =>
{
"title" : title,
"isDone" : isDone
};
}
This is my screen
class _TodoListScreenState extends State<TodoListScreen> {
List<Todo> todos = [];
//updates the state of the checkbox and reflects it on the UI
_toggleTodo(Todo todo, bool isChecked) {
setState(() {
todo.isDone = isChecked;
_deleteTodo(todo,isChecked);
print(todos.length);
});
}
_addTodo() async {
final todo = await showDialog<Todo>(
context: context,
builder: (BuildContext context) { // <- Here you draw the
Dialog
return NewTodoDialog();
},
);
if (todo != null) {
setState(() {
todos.add(todo);
_saveTodo(todos);
print(todos.length);
});
}
}
_deleteTodo (Todo todo, bool isDone) => (isDone)?
todos.remove(todo): debugPrint;
//Save you array object as an array of Strings in Shared Preferences
Future<void> _saveTodo(List<Todo> todo) async {
SharedPreferences sharedPreferences = await
SharedPreferences.getInstance();
sharedPreferences.setStringList("savedData", _mapTodoData(todo));
}
_mapTodoData(List<dynamic> todos) {
try {
var res = todos.map((v) => json.encode(v)).toList();
return res;
} catch (err) {
// Just in case
return ["Nope"];
}
}
Future<List> loadData() async {
SharedPreferences sharedPreferences = await
SharedPreferences.getInstance();
final List<Todo> todoArray =
_decodeTodoData(sharedPreferences.
getStringList("savedData")).toList();
todos = todoArray;
return todoArray;
}
List<Todo> _decodeTodoData(List<String> todos) {
try {
//Transforming List<String> to Json
var result = todos.map((v) => json.decode(v)).toList();
//Transforming the Json into Array<Todo>
var todObjects = result.map((v) => Todo.fromJson(v)).toList();
return todObjects;
} catch (error) {
return [];
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(backgroundColor: Colors.deepPurple[900],
title: Text('Todo List')),
body: Container(
child: FutureBuilder(
future: loadData(),
builder: (BuildContext context, AsyncSnapshot snapshot){
return TodoList(
todos: todos,
onTodoToggle: _toggleTodo,
);
})
)
,
floatingActionButton: FloatingActionButton(
backgroundColor: Colors.purpleAccent[700],
child: Icon(Icons.add),
onPressed: _addTodo,
),
);
}
}
Thanks in advance, hope someone can help me :)
From what I can tell is that your _deleteTodo deletes from the local instance list todos but you are instructing flutter to rebuild the UI from disk via loadData which re-gets the data from disk.
My suggestion is to get the data once on page load using initState and from there only refer to the local instance of todos.
In your _deleteTodo you would also need to persist the state to disk or have a commit button somewhere
I Found the solution, inside my _toggleTodo i need it to save my TodoList after doing something to my todo.
I have the following FutureBuilder function in class A:
Future<String> GetYoutubeLink() async{
var link = "";
CollectionReference collectionRef =
Firestore.instance.collection("r");
Query query = collectionRef.where('name',
isEqualTo: name).limit(1);
QuerySnapshot collectionSnapshot = await query.getDocuments().then((data){
if(data.documents.length > 0){
link = data.documents[0].data['link'];
print(link);
}
});
return link.toString();
}
}
I am trying to set the link in class B as follows:
class _B extends State<B> {
String link = null;
void initState(){
super.initState();
setState(() {
A a = new A(widget.dish_name);
if(link == null) {
link = a.GetYoutubeLink().toString();
}
});
}
#override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: Icon(FontAwesomeIcons.youtubeSquare, size: 45,color:Colors.red),
onPressed: _launchURL,
),
],
);
}
_launchURL() async {
var url = link;
if (await canLaunch(url)) {
await launch(url);
} else {
throw 'Could not launch $url';
}
}
}
I am getting the following exception:
Could not launch Instance of 'Future'
Can someone tell me how to get the string instead of Future ?
Just Modify your code like this..
_launchURL() async{
CollectionReference collectionRef =
Firestore.instance.collection("r");
Query query = collectionRef.where('name',
isEqualTo: name).limit(1);
QuerySnapshot collectionSnapshot = await query.getDocuments().then((data){
if(data.documents.length > 0){
link = data.documents[0].data['link'];
if (await canLaunch(url)) {
await launch(url);
} else {
throw 'Could not launch $url';
}
}
});
}
And no need for initState() just remove that call.
Class a's method:
Future<String> getYoutubeLink() async {//its a good practice to set the method's name in lowerCamelCase
CollectionReference collectionRef = Firestore.instance.collection("r");
Query query = collectionRef.where('name', isEqualTo: name).limit(1);
QuerySnapshot collectionSnapshot = await query.getDocuments().then((data) {
try {
if (data.documents.length > 0) {
return data.documents[0].data['link'].toString(); //this will return the data as a string
}
} catch (e) {
return ""; //in case that something fails will return an empty string
}
});
return ""; //if it do not return anything will return an empty string
}
Class b:
class _B extends State<B> {
A a = new A(widget.dish_name);
#override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: Icon(FontAwesomeIcons.youtubeSquare, size: 45,color:Colors.red),
onPressed: _launchURL,
),
],
);
}
_launchURL() async {
var url = await a.getYoutubeLink();//call here your method. You are useing 'await' because this methods returns a Future, it means that the execution of this function should take some time
if (await canLaunch(url)) {
await launch(url);
} else {
throw 'Could not launch $url';
}
}
}
Do not call Future methods inside your initState() because it would crash your app. If you need to call Future methods that need time to be completed before building your widget you can use the FutureBuilder widget. Check here the documentation.
To know more about Future and async programing watch this video by MTechViral. I have learned a lot from him!
I have a Flutter FutureBuilder that needs to be updated with new data given by the user. However, the UI elements in the FutureBuilder do not update and still contain the old values. I have checked through print statements that the new data is correctly loaded. The issue seems to be with FutureBuilder rebuilding the widget when the new data is loaded. Any help is appreciated.
Future<List<PollItem>> fetchPost(String loc) async {
return new Future(() async {
final response = await http
.post(restip + '/getPosts',
body: {"data": loc});
if (response.statusCode == 200) {
print(response.body);
// If the call to the server was successful, parse the JSON
// This function adds json to list
PollItem.fromJson(json.decode(response.body));
// list is a list of posts gathered based on the string criteria
return list;
} else {
throw Exception('Failed to load polls');
}
});
}
class PollState extends State<Poll> {
TextEditingController textc = new TextEditingController();
static String dropDowntext = "City";
String _name = "Search";
final _names = [''];
Widget build(BuildContext context) {
print("dropdown"+dropDowntext);
textc.text = _name;
print(dropDowntext);
return FutureBuilder<List<PollItem>>(
future: fetchPost(dropDowntext),
initialData: [PollItem()],
builder: (context, snapshot) {
if (snapshot.hasData) {
print(snapshot.data[0].question);
});
}
Here is my global file:
List<PollItem> list = new List();
factory PollItem.fromJson(Map<String, dynamic> json) {
int len = json['length'];
if(listNum!=len) {
listNum = len;
list.clear();
for (int i = 0; i < len; i++) {
list.add(PollItem(
answer1: json[i.toString()]['answer1'],
location: json[i.toString()]['location']
)
);
}
}
}
You don't need to create a Future object :
Future<List<PollItem>> fetchPost(String loc) async {
final response = await http.post(restip + '/getPosts',body: {"data": loc});
if (response.statusCode == 200) {
print(response.body);
final data = json.decode(response.body);
int len = data['length'];
final List<PollItem> newList = List();
for (int i = 0; i < len; i++) {
newList.add(PollItem(
answer1: data[i.toString()]['answer1'],
location: data[i.toString()]['location']
)
);
}
print("new list size: ${newList.length}");
return newList;
} else {
throw Exception('Failed to load polls');
}
return null;
}
I have API.dart like this to authenticating a user, log in and log out
class Api {
static FirebaseAuth _auth = FirebaseAuth.instance;
static GoogleSignIn _googleSignIn = GoogleSignIn();
FirebaseUser firebaseUser;
Api(FirebaseUser user) {
this.firebaseUser = user;
}
static Future<FBApi> signInWithGoogle() async {
final GoogleSignInAccount googleUser = await _googleSignIn.signIn();
final GoogleSignInAuthentication googleAuth = await googleUser.authentication;
final FirebaseUser user = await _auth.signInWithGoogle(
accessToken: googleAuth.accessToken,
idToken: googleAuth.idToken,
);
assert(user.email != null);
assert(user.displayName != null);
assert(await user.getIdToken() != null);
final FirebaseUser currentUser = await _auth.currentUser();
assert(user.uid == currentUser.uid);
// print('photoURL api ' + user.photoUrl);
return Api(user);
}
static Future<void> signOut() async {
await _auth.signOut().then((_) {
print("***** log out...what the hell?");
_googleSignIn.signOut();
});
}
}
I've have a cloud function to create new user to database cloud firestore.
And in view account settings, I want to update user information like displayName, photoUrl into firestore. How I get current user in my account setting view.
class Settings extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(
'ACCOUNT',
style: TextStyle(color: primaryColor, fontWeight: FontWeight.bold),
),
centerTitle: true,
),
body: new SettingsScreen(),
);
}
}
class SettingsScreen extends StatefulWidget {
#override
State createState() => new SettingsScreenState();
}
class SettingsScreenState extends State<SettingsScreen> {
TextEditingController controllerNickname;
SharedPreferences prefs;
String id;
String nickName;
String photoUrl;
bool isLoading = false;
File avatarImageFile;
final FocusNode focusNodeNickname = new FocusNode();
#override
void initState() {
super.initState();
readLocal();
}
void readLocal() async {
prefs = await SharedPreferences.getInstance();
id = prefs.getString('id') ?? '';
nickName = prefs.getString('nickName') ?? '';
photoUrl = prefs.getString('photoUrl') ?? '';
controllerNickname = new TextEditingController(text: nickName);
// Force refresh input
setState(() {});
}
Future getImage() async {
File image = await ImagePicker.pickImage(source: ImageSource.gallery);
if (image != null) {
setState(() {
avatarImageFile = image;
isLoading = true;
});
}
uploadFile();
}
Future uploadFile() async {
String fileName = id;
StorageReference reference = FirebaseStorage.instance.ref().child(fileName);
StorageUploadTask uploadTask = reference.putFile(avatarImageFile);
StorageTaskSnapshot storageTaskSnapshot;
uploadTask.onComplete.then((value) {
if (value.error == null) {
storageTaskSnapshot = value;
storageTaskSnapshot.ref.getDownloadURL().then((downloadUrl) {
photoUrl = downloadUrl;
Firestore.instance
.collection('users')
.document(id)
.updateData({'displayName': nickName, 'photoUrl': photoUrl}).then((data) async {
await prefs.setString('photoUrl', photoUrl);
setState(() {
isLoading = false;
});
Fluttertoast.showToast(msg: "Upload success");
}).catchError((err) {
setState(() {
isLoading = false;
});
Fluttertoast.showToast(msg: err.toString());
});
}, onError: (err) {
setState(() {
isLoading = false;
});
Fluttertoast.showToast(msg: 'This file is not an image');
});
} else {
setState(() {
isLoading = false;
});
Fluttertoast.showToast(msg: 'This file is not an image');
}
}, onError: (err) {
setState(() {
isLoading = false;
});
Fluttertoast.showToast(msg: err.toString());
});
}
void handleUpdateData() {
focusNodeNickname.unfocus();
setState(() {
isLoading = true;
});
Firestore.instance
.collection('users')
.document(id)
.updateData({'displayName': nickName, 'photoUrl': photoUrl}).then((data) async {
await prefs.setString('nickname', nickName);
await prefs.setString('photoUrl', photoUrl);
setState(() {
isLoading = false;
});
Fluttertoast.showToast(msg: "Update success");
}).catchError((err) {
setState(() {
isLoading = false;
});
Fluttertoast.showToast(msg: err.toString());
});
}
#override
Widget build(BuildContext context) {
...
You can do something like this FirebaseAuth.instance.currentUser()
This returns the current user if any. Otherwise it returns null
The ListView shows Item1, Item2 and Item3.
When I try to delete Item2, the ListView incorrectly shows Item1 and Item2.
The console shows the correct items in the list: Item1 and Item3.
HomeScreen:
class HomeScreen extends StatefulWidget {
#override
HomeScreenState createState() => new HomeScreenState();
}
class HomeScreenState extends State<HomeScreen> {
List<Todo> todos = new List();
#override
void initState() {
super.initState();
populateTodos();
}
void populateTodos() async {
TodoDatabase db = new TodoDatabase();
db.getAllTodos().then((newTodos) {
for (var todo in newTodos) {
print(todo.title + ", " + todo.id);
}
setState(() => todos = newTodos);
});
}
void openAddTodoScreen() async {
Navigator
.push(context,
new MaterialPageRoute(builder: (context) => new AddTodoScreen()))
.then((b) {
populateTodos();
});
}
void clearDb() async {
TodoDatabase db = new TodoDatabase();
db.clearDb();
populateTodos();
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Todo App"),
actions: <Widget>[
new IconButton(
icon: new Icon(Icons.delete),
onPressed: () => clearDb(),
),
new IconButton(
icon: new Icon(Icons.refresh),
onPressed: () => populateTodos(),
)
],
),
body: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Expanded(
child: new ListView.builder(
padding: new EdgeInsets.all(10.0),
itemCount: todos.length,
itemBuilder: (BuildContext context, int index) {
return new TodoItem(todos[index], onDelete: (id) {
TodoDatabase db = new TodoDatabase();
print("ID: " + id);
db.deleteTodo(id).then((b) {
populateTodos();
});
});
},
),
)
],
),
),
floatingActionButton: new FloatingActionButton(
child: new Icon(Icons.add), onPressed: () => openAddTodoScreen()),
);
}
}
DataBase Code:
class TodoDatabase {
TodoDatabase();
static Database _db;
Future<Database> get db async {
if (_db != null) {
return _db;
}
_db = await initDB();
return _db;
}
Future<Database> initDB() async {
Directory documentsDirectory = await getApplicationDocumentsDirectory();
String path = join(documentsDirectory.path, "main.db");
var theDb = await openDatabase(path, version: 1, onCreate: createDatabase);
return theDb;
}
void createDatabase(Database db, int version) async {
await db.execute("CREATE TABLE Todos(id STRING PRIMARY KEY, title TEXT, description TEXT)");
print("Database was Created!");
}
Future<List<Todo>> getAllTodos() async {
var dbClient = await db;
List<Map> res = await dbClient.query("Todos");
print(res);
return res.map((map) => new Todo(title: map["title"], description: map["description"], id: map["id"])).toList();
}
Future<Todo> getTodo(String id) async {
var dbClient = await db;
var res = await dbClient.query("Todos", where: "id = ?", whereArgs: [id]);
if (res.length == 0) return null;
return new Todo.fromDb(res[0]);
}
Future<int> addTodo(Todo todo) async {
var dbClient = await db;
int res = await dbClient.insert("Todos", todo.toMap());
return res;
}
Future<int> updateTodo(Todo todo) async {
var dbClient = await db;
int res = await dbClient.update(
"Todos",
todo.toMap(),
where: "id = ?",
whereArgs: [todo.id]);
return res;
}
Future<int> deleteTodo(String id) async {
var dbClient = await db;
var res = await dbClient.delete(
"Todos",
where: "id = ?",
whereArgs: [id]);
print("Deleted item");
return res;
}
Future<int> clearDb() async {
var dbClient = await db;
var res = await dbClient.execute("DELETE from Todos");
print("Deleted db contents");
return res;
}
}
This almost seems like a bug with Flutter.
I have to add some more text because otherwise it won't let me post.
I don't know how to illustrate the issue further.
Thanks for your help!
I was missing a key.
Here's the correct ListView.builder:
new ListView.builder(
key: new Key(randomString(20)),
padding: new EdgeInsets.all(10.0),
itemCount: todos.length,
itemBuilder: (BuildContext context, int index) {
return new TodoItem(todos[index], onDelete: (id) {
TodoDatabase db = new TodoDatabase();
db.deleteTodo(id).then((b) {
populateTodos();
});
});
},
),