I am building a ridesharing app with flutter. So far i am stuck on how to communicate between the rider and driver app.
After rider submits pickup request to firestore db, i want the loading screen to show until a driver accepts the request(possibly by updating firestore db) then move to screen with driver info.
if (event is PaymentMadeEvent) {
yield TaxiBookingLoadingState(
state:
PaymentNotInitializedState(booking: null, methodsAvaiable: null));
TaxiBooking booking = await TaxiBookingStorage.addDetails(TaxiBooking.named(paymentMethod: event.paymentMethod));
String docID = await TaxiBookingController.submitRequest(
booking.source.areaDetails,
booking.destination.areaDetails,
[booking.source.position.latitude,booking.source.position.longitude],
[booking.destination.position.latitude,booking.destination.position.longitude],
booking.estimatedPrice,
booking.estimatedDuration,
booking.estimatedDistance
);
booking = await TaxiBookingStorage.addDetails(TaxiBooking.named(dbID: docID));
await TaxiBookingController.generateToken();
TaxiDriver taxiDriver = await TaxiBookingController.getTaxiDriver(booking);
// Timer.periodic(Duration(seconds: 5), (Timer t) async* {
// } );
taxiDriver = await TaxiBookingController.getTaxiDriver(booking);
yield TaxiNotConfirmedState(booking: booking, driver: taxiDriver);
}
static Future<TaxiDriver> getTaxiDriver(TaxiBooking booking) async {
TaxiDriver taxis2;
var driver = await Firestore.instance.collection("rider_pickup_pairing")
// .where(DocumentReference,isEqualTo: booking.dbID)
.where("driver_id",isEqualTo: 'jk')
.getDocuments()
.then((QuerySnapshot snapshot) {
if (snapshot.documents == []) {
taxis2 = null;
} else {
snapshot.documents.forEach((f) =>
taxis2 = TaxiDriver.named(
driverPic:
"https://upload.wikimedia.org/wikipedia/commons/thumb/e/e3/profilepic.jpg",
driverName: "John Doe",
driverRating: 4.5,
taxiDetails: "Toyota Corolla(ABJ823KU)")
);
TaxiBookingStorage.addDetails(TaxiBooking.named(driver: taxis2.driverName));
}
return taxis2;
});
return driver;
}
You should be using .onSnapshot() instead of .getDocument() in order to achieve this.
The difference between these two methods is that getDocument() will only retrieve the document once while onSnapshot() will keep listening to any event on Firestore. This is covered in these documents: get realtime updates and get data once.
Hope you find this useful.
Related
I am having trouble trying to fetch data from firebase and updating the values from it.
I have a restaurant name and the number of times it has been picked (user chooses to go to that restaurant to eat). I am trying to retrieve the numPicked and update it by adding one if the user decides to go there again.
Here i am trying to fetch ONE specific document and trying to store the docID and the variables I need to update.
docID = doc.id; docID is return NULL
meaning that the foreach loop isn't even being read.
Future<bool> searchQuery(
{required String restaurantName,
required var userID,
required db}) async {
int addOne = 1; //addes one if it has been picked
//this is not working
try {
Query query2 =
db.where('userId', isEqualTo: FirebaseAuth.instance.currentUser!.uid);
Query query = query2.where('restaurantName', isEqualTo: restaurantName);
await query.get().then((querySnapshot) {
// ignore: avoid_function_literals_in_foreach_calls
querySnapshot.docs.forEach((doc) {
docID = doc.id;
numPicked = doc['numPicked'];
restaurantExist = true;
});
}).catchError((error) {
// print('error querying: #error');
});
} catch (ex) {
// ignore: avoid_print
print(ex);
}
//this is not working
int totalPicked = numPicked + addOne;
//if the restaurant exist then update the numpicked for that specific restaurant
if (restaurantExist) {
try {
var query = db
//.collection('NumRestaurantPicked')
.doc(docID);
await query.update({'numPicked': totalPicked.toString()});
} catch (ex) {}
}
return restaurantExist;
}
The docID and numPicked variables are not defined in the method signature, so they are not accessible outside of the try block. They should be defined as class variables, so they can be accessed from other methods.
I have the following function which gets a product document from Firestore, to be used with a FutureBuilder:
Future<Product> getProduct(String productId) async {
var ref = _db.collection('products').doc(productId);
var snapshot = await ref.get();
return Product.fromJson(snapshot.data() ?? {});
}
I can I achieve the same functionality, but with a StreamBuilder so that the build method will be called any time there's a change to the document?
Stream<Product> listenToProduct(String productId) {
?
};
Well, this seems to work:
Stream<Product> listenToProduct(String productId) {
return _db.collection('products')
.doc(productId)
.snapshots()
.map((snapshot) => Product.fromJson(snapshot.data()!));
}
Stream<Product> listenToProduct(String productId) {
return _db.collection('products').doc(productId).snapshots()
.listen((event) => Product.fromJson(event.data() ?? {});
}
Something similar to that.
I am trying to make a chat app with the help of a channel,
there is a search page where I search user to chat,
If I tap on user, a new windows will be created if no previous chat found,
and if not chatting first time, will use existing chatroom
code is running well, but I want some implement ,
if I search a user and tap on him, and than I go back without chatting, created new room should be deleted.... so that I need number of message's logic...
how to implement to achieve it
Future<ChatRoomModel?> getchatroom(UserModel targetuser) async {
ChatRoomModel? chatroom;
//here i feel something wrong as even if blocked chatroom, window should be open
QuerySnapshot querysnapshot = await FirebaseFirestore.instance
.collection("chatrooms")
.where("participants.${targetuser.uid}", isEqualTo: true)
.where("participants.${widget.userModel.uid}", isEqualTo: true)
.get();
if (querysnapshot.docs.length > 0) {
var docdata = querysnapshot.docs[0].data();
ChatRoomModel existingchatroom =
ChatRoomModel.fromMap(docdata as Map<String, dynamic>);
chatroom = existingchatroom;
} else {
//creating new chat room
ChatRoomModel newchatroommodel = ChatRoomModel(
chatroomid: DateTime.now().toString(),
participants: {
widget.userModel.uid.toString(): true,
targetuser.uid.toString(): true,
},
lastMessage: "Say Hi");
await FirebaseFirestore.instance
.collection("chatrooms")
.doc(newchatroommodel.chatroomid)
.set(newchatroommodel.toMap());
chatroom = newchatroommodel;
print("Created success");
}
return chatroom;
}
Delete your whole chat via 'ChatRoomId'
FirebaseFirestore.instance
.collection("chatrooms/'your_chat_room_id'")
.delete()
.then((value_2) {
print('========> successfully deleted');
});
Count your messages by retrieving a list of documents from your "messages" collection:
QuerySnapshot messages = await querysnapshot.docs[0].reference.collection("messages").get();
int messageCount = messages.size;
How to properly implement pagination with firestore stream on flutter (in this case flutter web) ?
my current approach with bloc which is most likely wrong is like this
function called on bloc when load next page, notice that i increased the lastPage variable of the state by 1 each time the function is called:
Stream<JobPostingState> _loadNextPage() async* {
yield state.copyWith(isLoading: true);
try {
service
.getAllDataByClassPage(state.lastPage+1)
.listen((List<Future<DataJob>> listDataJob) async {
List<DataJob?> listData = [];
await Future.forEach(listDataJob, (dynamic element) async {
DataJob data= await element;
listData.add(data);
});
bool isHasMoreData = state.listJobPostBlock.length!=listData.length;
//Update data on state here
});
} on Exception catch (e, s) {
yield StateFailure(error: e.toString());
}}
function called to get the stream data
Stream<List<Future<DataJob>>> getAllDataByClassPage(
String className, int page) {
Stream<QuerySnapshot> stream;
if (className.isNotEmpty)
stream = collection
.orderBy('timestamp', "desc")
.where('class', "==", className).limit(page*20)
.onSnapshot;
else
stream = collection.onSnapshot;
return stream.map((QuerySnapshot query) {
return query.docs.map((e) async {
return DataJob.fromMap(e.data());
}).toList();
});
}
With this approach it works as intended where the data loaded increased when i load next page and still listening to the stream, but i dont know if this is proper approach since it replace the stream could it possibly read the data twice and end up making my read count on firestore much more than without using pagination. Any advice is really appreciated, thanks.
Your approach is not very the best possible indeed, and as you scale you going to be more costly. What I would do in your shoes would be to create a global variable that represents your stream so you can manipulate it. I can't see all of your code so I am going to be as generic as possible so you can apply this to your code.
First let's declare the stream controller as a global variable that can hold the value of your stream:
StreamController<List<DocumentSnapshot>> streamController =
StreamController<List<DocumentSnapshot>>();
After that we need to change your getAllDataByClassPage function to the following:
async getAllDataByClassPage(String className) {
Stream stream = streamController.stream;
//taking out of the code your className logic
...
if(stream.isEmpty){
QuerySnapshot snap = await collection.orderBy('timestamp', "desc")
.where('class', "==", className)
.limit(20)
.onSnapshot
streamController.add(snap.docs);
}else{
DocumentSnapshot lastDoc = stream.last;
QuerySnapshot snap = await collection.orderBy('timestamp', "desc")
.where('class', "==", className)
.startAfterDocument(lastDoc)
.limit(20)
.onSnapshot;
streamController.add(snap.docs);
}
}
After that all you need to do in order to get the stream is invoke streamController.stream;
NOTE: I did not test this code but this is the general ideal of what you should try to do.
You can keep track of last document and if has more data on the list using startAfterDocument method. something like this
final data = await db
.collection(collection)
.where(field, arrayContains: value)
.limit(limit)
.startAfterDocument(lastDoc)
.get()
.then((snapshots) => {
'lastDoc': snapshots.docs[snapshots.size - 1],
'docs': snapshots.docs.map((e) => e.data()).toList(),
'hasMore': snapshots.docs.length == limit,
});
I wrote a StreamProvider that I listen to right after startup to get all the information about a potentially logged in user. If there is no user, so the outcome would be null, the listener stays in loading state, so I decided to send back a default value of an empty user to let me know that the loading is done.
I had to do this, because Hive's watch() method is only triggered when data changes, which it does not at startup.
So after that, I want the watch() method to do its job, but the problem with that, are the following scenarios:
At startup: No user - Inserting a user -> watch method is triggered -> I get the inserted users data -> Deleting the logged in user -> watch method is not triggered.
At startup: Full user - Deleting the user -> watch method is triggered -> I get an empty user -> Inserting a user -> watch method is not triggered.
After some time I found out that I can make use of all CRUD operations as often as I want to and the Hive's box does what it should do, but the watch() method is not triggered anymore after it got triggered once.
The Streamprovider(s):
final localUsersBoxFutureProvider = FutureProvider<Box>((ref) async {
final usersBox = await Hive.openBox('users');
return usersBox;
});
final localUserStreamProvider = StreamProvider<User>((ref) async* {
final usersBox = await ref.watch(localUsersBoxFutureProvider.future);
yield* Stream.value(usersBox.get(0, defaultValue: User()));
yield* usersBox.watch(key: 0).map((usersBoxEvent) {
return usersBoxEvent.value == null ? User() : usersBoxEvent.value as User;
});
});
The Listener:
return localUserStream.when(
data: (data) {
if (data.name == null) {
print('Emitted data is an empty user');
} else {
print('Emitted data is a full user');
}
return Container(color: Colors.blue, child: Center(child: Row(children: [
RawMaterialButton(
onPressed: () async {
final globalResponse = await globalDatabaseService.signup({
'email' : 'name#email.com',
'password' : 'password',
'name' : 'My Name'
});
Map<String, dynamic> jsonString = jsonDecode(globalResponse.bodyString);
await localDatabaseService.insertUser(User.fromJSON(jsonString));
},
child: Text('Insert'),
),
RawMaterialButton(
onPressed: () async {
await localDatabaseService.removeUser();
},
child: Text('Delete'),
)
])));
},
loading: () {
return Container(color: Colors.yellow);
},
error: (e, s) {
return Container(color: Colors.red);
}
);
The CRUD methods:
Future<void> insertUser(User user) async {
Box usersBox = await Hive.openBox('users');
await usersBox.put(0, user);
await usersBox.close();
}
Future<User> readUser() async {
Box usersBox = await Hive.openBox('users');
User user = usersBox.get(0) as User;
await usersBox.close();
return user;
}
Future<void> removeUser() async {
Box usersBox = await Hive.openBox('users');
await usersBox.delete(0);
await usersBox.close();
}
Any idea how I can tell the StreamProvider that the watch() method should be kept alive, even if one value already got emitted?
but the watch() method is not triggered anymore after it got triggered
once
Thats because after every CRUD you're closing the box, so the stream (which uses that box) stop emitting values. It won't matter if you're calling it from somewhere outside riverpod (await Hive.openBox('users')) its calling the same reference. You should close the box only when you stop using it, I would recommend using autodispose with riverpod to close it when is no longer used and maybe put those CRUD methods in a class controlled by riverpod, so you have full control of the lifecycle of that box
final localUsersBoxFutureProvider = FutureProvider.autoDispose<Box>((ref) async {
final usersBox = await Hive.openBox('users');
ref.onDispose(() async => await usersBox?.close()); //this will close the box automatically when the provider is no longer used
return usersBox;
});
final localUserStreamProvider = StreamProvider.autoDispose<User>((ref) async* {
final usersBox = await ref.watch(localUsersBoxFutureProvider.future);
yield* Stream.value(usersBox.get(0, defaultValue: User()) as User);
yield* usersBox.watch(key: 0).map((usersBoxEvent) {
return usersBoxEvent.value == null ? User() : usersBoxEvent.value as User;
});
});
And in your methods use the same instance box from the localUsersBoxFutureProvider and don't close the box after each one, when you stop listening to the stream or localUsersBoxFutureProvider it will close itself