I have a ListView, filled with Firebase collection as chat messages.
When the user long press a ListTile (an item on the list), I show a BottomSheet.
When the user clicks the Delete button on the BottomSheet, the document should be removed. I'm unable to find a way to remove the selected document after tapping the "Delete" button.
The listview widget:
return ListView.builder(
padding: new EdgeInsets.all(8.0),
reverse: true,
itemCount: snapshot.data.documents.length,
itemBuilder: (_, int index) {
var message = snapshot.data.documents[index]["content"];
var from = snapshot.data.documents[index]["idFrom"];
return new ListTile(
leading: new CircleAvatar(
backgroundColor: Colors.blue,
child: new Image.network(
"http://res.cloudinary.com/kennyy/image/upload/v1531317427/avatar_z1rc6f.png")),
title: new Row(
children: <Widget>[
new Expanded(child: new Text(message))
]
),
subtitle: new Text(from),
onLongPress: () {
showModalBottomSheet<void>(context: context,
builder: (BuildContext context) {
return Container(
child: new Wrap(
children: <Widget>[
new ListTile(
leading: new Icon(Icons.delete),
title: new Text('Delete'),
onTap: () =>
// Remove the tapped document here - how?
print(snapshot.data.documents[index]["id"])
Firestore.instance.collection("chats").document("ROOM_1")
.collection("messages").document(snapshot.data.documents[index]["id"])
.delete();
)
)
]
)
);
});
}
);
});
I need to find a way to remove the document in the onTap() method. not sure why, but even when I do:
onTap: () => {
// Remove the tapped document here - how?
const id = snapshot.data.documents[index]["id"];
},
the IDE retruns an error Expected an identifier
To delete a document, we can use the runTransaction method of the Firestore.instance and use the delete method of the Transaction class.
await Firestore.instance.runTransaction((Transaction myTransaction) async {
await myTransaction.delete(snapshot.data.documents[index].reference);
});
instead of
Firestore.instance.collection("chats").document("ROOM_1")
.collection("messages").document(snapshot.data.documents[index]["id"])
.delete();
Here is the look of my dummy database before the deletion of doc1 document:
After deleting doc1:
If you know document id, You can delete easily.
Future<void> rejectJob(String jobId){
return _db.collection('jobs').document(jobId).delete();
}
Update 2021:
Add the following code to your button's onPressed method.
final collection = FirebaseFirestore.instance.collection('collection');
collection
.doc('some_id') // <-- Doc ID to be deleted.
.delete() // <-- Delete
.then((_) => print('Deleted'))
.catchError((error) => print('Delete failed: $error'));
.delete() method will only remove the document fields but will not remove the nested collections in the document.
Use runTransactions() method to delete the document completely without leaving any data behind.
FireBaseFirestore.instance.runTransactions((transaction) async =>
await transaction.delete(DocumentReference));
So, use runTransactions() instead of delete() method.
for simple and quick method try this (especially if you use login/signup)
Future deleteData(String id) async{
try {
await FirebaseFirestore.instance
.collection("users")
.doc(FirebaseAuth.instance.currentUser!.uid)
.collection("collection_name")
.doc(id)
.delete();
}catch (e){
return false;
}
}
then under the delete button you can pass the deleteData function
onPressed:() {
deleteData(collection_name.id);
Navigator.pop(context);
}, //delete
Related
In flutter, I use Firebase Firestore to save data. I created the root collection as Users and in that collection, each user has a subcollection named by Books.
This is my Firebase Cloud Firestore
I tried to get all users all books with a button click as below
getUserBooks() {
FirebaseFirestore.instance.collection("Users").get().then((querySnapshot) {
querySnapshot.docs.forEach((result) {
FirebaseFirestore.instance
.collection("Users")
.doc(result.id)
.collection("Books")
.get()
.then((querySnapshot) {
querySnapshot.docs.forEach((result) {
BooksModel booksModel = BooksModel.fromDocument(result);
print("--------------------- Books ---------------------\n"
"id: ${booksModel.bookID}\n"
"name: ${booksModel.bookName}\n"
"image: ${booksModel.bookImage}");
});
});
});
});
}
This is model class
class BooksModel {
final String bookID;
final String bookName;
final String bookImage;
BooksModel({
this.bookID,
this.bookName,
this.bookImage,
});
factory BooksModel.fromDocument(DocumentSnapshot documentSnapshot) {
return BooksModel(
bookID: documentSnapshot['bookID'],
bookName: documentSnapshot['bookName'],
bookImage: documentSnapshot['bookImage'],
);
}
}
I used the below code to get the book details according to a single user but I want to get all users books details to that widget. How can I change the code?
_usersBookListWidget() {
return Container(
height: MediaQuery.of(context).size.height,
child: StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance
.collection("Users")
.doc(userID)
.collection('Books')
.snapshots(),
builder: (context, snapshot) {
return !snapshot.hasData
? Container()
: snapshot.data.docs.length.toString() == "0"
? Container(
height: 250.0,
width: 200.0,
child: Column(
children: [
SizedBox(
height: 30.0,
),
Text(
"You have no books yet",
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.headline6
.fontSize),
),
Image.asset(
'assets/icons/icon_no_books_yet.png',
height: 100.0,
width: 100.0,
),
],
),
)
: ListView.builder(
scrollDirection: Axis.vertical,
physics: BouncingScrollPhysics(),
itemCount: snapshot.data.docs.length,
itemBuilder: (BuildContext context, int index) {
BooksModel booksModel =
BooksModel.fromDocument(snapshot.data.docs[index]);
return Column(
children: [
Text(booksModel.bookID),
Text(booksModel.bookName),
],
);
},
);
},
),
);
}
There is not a direct way yet to get all sub-collections for all the documents in a single collection like in your case.
First you have to query your users collection, then after that, for each user document, you run a separate query. Here's a pseudo code:
1- StreamBuilder (FirebaseFirestore.instance.collection.('Users').snapshots(),
Here you can display info about every user in a listview if you want, but you have to capture the document.id, because you will need it for the next step.
2- you create a widget called userBooks for example, for this widget, you pass to it the document.id from the previous step. Now you have all the user IDs in your database, and since the subcollection is called books and doesn't change, you use this for another streambuilder, with this collection reference:
FirebaseFirestore.instance.collection.('Users').doc(document.id).collection('Books').snapshots(). This will give you the result you want.
For your code snippet to work, you need to use async\await, like this:
getUserBooks() async {
await FirebaseFirestore.instance.collection("Users").get().then((querySnapshot) async {
querySnapshot.docs.forEach((result) {
await FirebaseFirestore.instance
.collection("Users")
.doc(result.id)
.collection("Books")
.get()
.then((querySnapshot) {
querySnapshot.docs.forEach((result) {
BooksModel booksModel = BooksModel.fromDocument(result);
print("--------------------- Books ---------------------\n"
"id: ${booksModel.bookID}\n"
"name: ${booksModel.bookName}\n"
"image: ${booksModel.bookImage}");
});
});
});
});
}
This will work for getting them in a single button.
I am trying to get user avatar from firebase storage, however, my current code only returns Instance of 'Future<String>' even I am using async/await as below. How is it possible to get actual download URL as String, rather Instance of Future so I can access the data from CachedNewtworkImage?
this is the function that calls getAvatarDownloadUrl with current passed firebase user instance.
myViewModel
FutureOr<String> getAvatarUrl(User user) async {
var snapshot = await _ref
.read(firebaseStoreRepositoryProvider)
.getAvatarDownloadUrl(user.code);
if (snapshot != null) {
print("avatar url: $snapshot");
}
return snapshot;
}
getAvatarURL is basically first calling firebase firestore reference then try to access to the downloadURL, if there is no user data, simply returns null.
Future<String> getAvatarDownloadUrl(String code) async {
Reference _ref =
storage.ref().child("users").child(code).child("asset.jpeg");
try {
String url = await _ref.getDownloadURL();
return url;
} on FirebaseException catch (e) {
print(e.code);
return null;
}
}
I am calling these function from HookWidget called ShowAvatar.
To show current user avatar, I use useProvider and useFuture to actually use the data from the database, and this code works with no problem.
However, once I want to get downloardURL from list of users (inside of ListView using index),
class ShowAvatar extends HookWidget {
// some constructors...
#override
Widget build(BuildContext context) {
// get firebase user instance
final user = useProvider(accountProvider.state).user;
// get user avatar data as Future<String>
final userLogo = useProvider(firebaseStoreRepositoryProvider)
.getAvatarDownloadUrl(user.code);
// get actual user data as String
final snapshot = useFuture(userLogo);
// to access above functions inside of ListView
final viewModel = useProvider(myViewModel);
return SingleChildScrollView(
physics: AlwaysScrollableScrollPhysics(),
child: Container(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
height: 100,
width: 100,
child: Avatar(
avatarUrl: snapshot.data, // **this avatar works!!!** so useProvider & useFuture is working
),
),
SizedBox(height: 32),
ListView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
return Center(
child: Column(
children: [
SizedBox(
height: 100,
width: 100,
child: Avatar(
avatarUrl: viewModel
.getAvatarUrl(goldWinners[index].user)
.toString(), // ** this avatar data is not String but Instance of Future<String>
),
),
),
],
),
);
},
itemCount: goldWinners.length,
),
Avatar() is simple statelesswidget which returns ClipRRect if avatarURL is not existed (null), it returns simplace placeholder otherwise returns user avatar that we just get from firebase storage.
However, since users from ListView's avatarUrl is Instance of Future<String> I can't correctly show user avatar.
I tried to convert the instance to String multiple times by adding .toString(), but it didn't work.
class Avatar extends StatelessWidget {
final String avatarUrl;
final double radius;
final BoxFit fit;
Avatar({Key key, this.avatarUrl, this.radius = 16, this.fit})
: super(key: key);
#override
Widget build(BuildContext context) {
print('this is avatar url : ' + avatarUrl.toString());
return avatarUrl == null
? ClipRRect(
borderRadius: BorderRadius.circular(radius),
child: Image.asset(
"assets/images/avatar_placeholder.png",
fit: fit,
),
)
: ClipRRect(
borderRadius: BorderRadius.circular(radius),
child: CachedNetworkImage(
imageUrl: avatarUrl.toString(),
placeholder: (_, url) => Skeleton(radius: radius),
errorWidget: (_, url, error) => Icon(Icons.error),
fit: fit,
));
}
}
Since the download URL is asynchronously determined, it is returned as Future<String> from your getAvatarUrl method. To display a value from a Future, use a FutureBuilder widget like this:
child: FutureBuilder<String>(
future: viewModel.getAvatarUrl(goldWinners[index].user),
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
return snapshot.hashData
? Avatar(avatarUrl: snapshot.data)
: Text("Loading URL...")
}
)
Frank actually you gave an good start but there are some improvements we can do to handle the errors properly,
new FutureBuilder(
future: //future you need to pass,
builder: (context, snapshot) {
if (snapshot.hasData) {
return new ListView.builder(
itemCount: snapshot.data.docs.length,
itemBuilder: (context, i) {
DocumentSnapshot ds = snapshot.data.docs[i];
return //the data you need to return using /*ds.data()['field value of doc']*/
});
} else if (snapshot.hasError) {
// Handle the error and stop rendering
GToast(
message:
'Error while fetching data : ${snapshot.error}',
type: true)
.toast();
return new Center(
child: new CircularProgressIndicator(),
);
} else {
// Wait for the data to fecth
return new Center(
child: new CircularProgressIndicator(),
);
}
}),
Now if you are using a text widget as a return statement in case of errors it will be rendered forever. Incase of Progress Indicators, you will exactly know if it is an error it will show the progress indicator and then stop the widget rendering.
else if (snapshot.hasError) {
}
else {
}
above statement renders until, if there is an error or the builder finished fetching the results and ready to show the result widget.
i m building an app using flutter and firebase as backend .
i m storring my data in a collection named 'Users' . every user has data and has a subcollection named 'Transactions' which are the transactions that he made .
i want to get these informations
Here is my DataBase
enter image description here
enter image description here
class DatabaseService {
final String uid;
DatabaseService({this.uid});
final CollectionReference usersCollection =
FirebaseFirestore.instance.collection('Users');
List<Transaction1> TransactionsListFromSnapshot(QuerySnapshot snapshot) {
return snapshot.docs.map((doc) {
return Transaction1(
uid: doc.data()['uid'],
name: doc.data()['name'],
description: doc.data()['description'],
time: doc.data()['time'],
argent: doc.data()['argent'],
somme: doc.data()['somme'],
deleted: doc.data()['deleted']);
}).toList();
}
Stream<List<Transaction1>> get transactions{
return
usersCollection.doc(uid).collection('Transactions').snapshots().map(TransactionsListFromSnapshot);
}
}
and this is the flutter code
body: StreamBuilder<List<Transaction1>>(
stream: DatabaseService(uid: this.widget.uid).transactions,
builder: (context, snapshot) {
return snapshot.hasData ? Stack(
children: [
ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, index) {
print('hhhhh');
return Transaction_Card(data: snapshot.data[index]);
},
),
Positioned(
right: 20,
bottom: 20,
child: FloatingActionButton(
child: Icon(
Icons.add,
color: Colors.white,
),
onPressed: () {
_showMyDialog();
},
))
],
) : Stack(
children: [
Text('Error'),
Positioned(
right: 20,
bottom: 20,
child: FloatingActionButton(
child: Icon(
Icons.add,
color: Colors.white,
),
onPressed: () {
_showMyDialog();
},
))
],
);
}),
Can anyone help me please
I just did this yesterday. In my parent document I add an array in which I store the document id of the sub collection documents.
Then, in the repository I have the following function (projects are the main documents, areas are the sub collection documents
#override
Stream<List<Area>> areas(String projectId) {
final CollectionReference areaCollection =
FirebaseFirestore.instance.collection('projects').doc(projectId).collection('areas');
return areaCollection.snapshots().map((snapshot) {
return snapshot.docs
.map((doc) => Area.fromEntity(AreaEntity.fromSnapshot(doc)))
.toList();
});
}
Then, I have used the example of the bloc documentation with entities and models to transform the data.
For each (sub) collection I have a separate listener, so one for projects, and one for areas of my currently focussed project. If new areas arrive, I check whether I already know them (by id) or if I have to add a new area to the project.
for (var a in event.areas) {
if (!areaPointer.containsKey(a.areaId)) { // add new area to list
areaPointer[a.areaId] = project.areas.length;
project.areas.add(a);
}
else // update existing area
project.areas[areaPointer[a.areaId]] = a;
}
As you see, I also have an areaPointer which stores the relationship of area.id and index of project.areas[i]
I am trying to build an app with category list. I wanted to make the selected category bigger and give it a bold color. But every time i set state, it refreshes the screen and then it sets the state. Can i stop it from refreshing every time i set a state?
this is my code
This is my Future
Future getCategories() async{
var firestore = Firestore.instance;
QuerySnapshot querySnapshot = await firestore.collection("category").getDocuments();
return querySnapshot.documents;
}
and this is my future builder
FutureBuilder(
future: getCategories(),
builder: (_, snapshot){
if (snapshot.connectionState == ConnectionState.waiting) {
return Container(
child: Text('Loading'),
);
}else return ListView(
scrollDirection: Axis.horizontal,
children:
List.generate(snapshot.data.length, (int index) =>
InkWell(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(snapshot.data[index].data["category"],
style: TextStyle(fontSize: currentCategory == snapshot.data[index].data["category"] ? sActive : sNotActive,
color: currentCategory == snapshot.data[index].data["category"] ? active : notActive,)
,),
),
onTap: () {
String selectedCategory = snapshot.data[index].data["category"];
setState(() {
currentCategory = selectedCategory;
print(selectedCategory);
});
},
)));
})
When you use setState() the entire widget will rebuild and subsequently the FutureBuilder which will call the getCategories() method again.
You can call the getCategories() method (without awaiting it) in the initState() and save the future in a property of the state class and then use this property in the future builder instead of getCategories().
Another solution could be to move the ListView() in a separate widget, so you can rebuild only the ListView when you select an item and call setState.
Anyway you can use for example the BLoC pattern to manage the state, in this way you don't need to rebuild the entire widget.
I'd like to check if a collection (the users collection) exists in the Firestore database or not. But I cannot find any means of doing that. If the collection exists, I would like to stream its documents, otherwise an empty stream as you see in the following method
- Is there any way to find a collection exists without getting its snapshots?
- Why break; or yield* Stream.empty() hangs the stream, like an endless stream!
Stream<userInfo> getCurrentUserInfos() async* {
final String usersCollectionPath = "users";
Stream<QuerySnapshot> snapshots = Firestore.instance.collection(usersCollectionPath).snapshots();
snapshots.isEmpty.then((hasNoUserCollection) {
// Although I don't have 'users' collection
// in my database but I never reach this point!
// i.e. hasNoUserCollection is always FALSE!
if(hasNoUserCollection) {
print("users collection doesn't exist, create it!");
// next line (i.e. `break;`) hangs the tool!
// And sometimes hangs the VSCode's IDE as well, if I have a breakpoint on it!!!
break;
// I tried yielding an empty stream instead of break, again another hang!
// yield* Stream<userInfo>.empty();
} else {
// previous stream is dead, so create it again!
snapshots = Firestore.instance.collection(usersCollectionPath ).snapshots();
await for (QuerySnapshot snap in snapshots) {
for (DocumentSnapshot userDoc in snap.documents) {
yield (new userInfo.fromQuerySnapshot(userDoc));
}
}
});
}
Now even a try-catch block cannot catch what's gone wrong, when the stream is empty!
try{
getCurrentUserInfos().last.then((userInfolastOne) {
print("last one: $lastOne.name");
});
// the following line (i.e. await ...) at least doesn't hang and
// `catch` block is able to catch the error `Bad state: No element`,
// when the stream is empty
//
// userInfo lastOne = await stream.last;
} catch (ex) {
print ("ex: $ex");
}
There is no API to detect if a collection exists. In fact: a collection in Firestore only exists if there are documents in it.
The cheapest check I can think of is doing a query for a single document, and then checking if that query has any results.
Okay, maybe I figured it out
final snapshot = await Firestore.instance.collection(collectionName).getDocuments();
if (snapshot.documents.length == 0) {
//Doesn't exist
}
This worked for me
As stated by #Frank, a collection in Firestore gets deleted if no Documents exist in it.
However, I understand that there might be cases where you want to keep a history of the collection modification/ creation events, or let's say for some reason prevent Collections from being deleted.
Or for example, you want to know when a collection was created in the first place. Normally, if the Documents are deleted, and then the Collection gets created again, you will not know the initial creation date.
A workaround I can think of is the following:
Initialize each collection you want with a Document that will be specifically for keeping generic info about that collection.
For example:
This way, even if all other Documents in the Collection are deleted, you'll still keep the Collection in addition to some info that might be handy if In the future you need to get some history info about the Collection.
So to know if a Collection exists of no, you can run a query that checks for a field in Info Documents (eg CollectionInfo.exists) to know which Collections have been already created.
This is a sample from one of my projects. You can use snapshot.data!.docs.isEmpty to check if a collection has data or not.
StreamBuilder(
stream: _billGroupStream,
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError) {
return const Center(
child: Text('Something went wrong'),
);
} else if (snapshot.connectionState == ConnectionState.waiting) {
return Center(
child: Column(
children: const [
LinearProgressIndicator(),
Text('Loading data, please wait...'),
],
),
);
} else if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.data!.docs.isEmpty) {
return const Center(
child: Text(
'Huh!! Looks like you have no transactions yet!' ,
textAlign: TextAlign.center,
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
),
);
} else if (snapshot.connectionState == ConnectionState.active) {
final List<DocumentSnapshot> docs = snapshot.data!.docs;
return ListView.builder(
shrinkWrap: true,
restorationId: 'billModeList',
itemCount: snapshot.data!.docs.length,
itemBuilder: (context, index) {
///This document snapshot is used for determining the unique id for update and delete methods
final DocumentSnapshot doc = docs[index];
//final DocumentSnapshot doc = snapshot.data!.docs[index];
///This [BillModel] converted data is used to build widgets
final BillModel billModel = doc.data()! as BillModel;
return Dismissible(
onDismissed: (direction) {
_remoteStorageService.deleteItemFromGroup(
widget.uri, doc.id);
setState(() {
docs.removeAt(index);
});
},
background: Container(
color: Colors.red,
child: const Icon(Icons.delete_forever_sharp),
),
key: Key(doc.id),
child: Card(
elevation: 3,
shadowColor: Colors.teal.withOpacity(.5),
child: ListTile(
leading:
const CircleAvatar(child: Icon(Icons.attach_money)),
title: Text(billModel.name),
subtitle: Text(billModel.category ?? ''),
trailing: Text(billModel.amount.toString()),
),
),
);
},
);
}
return const CircularProgressIndicator();
},
),