Vue3, firestore, useFirestore: How to get notified when binding is ready? - google-cloud-firestore

I'm currently migrating my project from Vue2+Vuex to Vue3+pinia. The storage of choice is still firebase. To keep my local storage in sync with the remote firebase-database I want to setup a (one-way) binding. I plan to use useFirestore as documented here.
I wrapped in a pinia action:
async bindTodos () {
this.todos = todos = useFirestore(collection(db, 'todos'));
}
with this.todos being defined in pinias state.
How can I maintain a boolean in the pinia state (i.e. todosReady) that get's updated when the todos were received the first time. Vuex/bindFirestoreRef used to return a promise. Or I am supposed to use import {watch} from "vue" and simply watch for changes in the returned Ref? Hope I'm on the right track!

Related

Firebase realtime database: load & monitor children (Swift) - best practice?

This seems like it ought to have a very simple answer.
I am building an app using SwiftUI and firebase realtime database.
The database will have a node called items with a large number of children - on the order of 1,000.
I'd like the app to load the contents of that node children when it launches, then listen to firebase for future additions of children. Imagine the following:
struct Item { … } // some struct
var items: [Item] = []
let itemsRef = Database.database().reference().child("items")
According to firebase, this call ought to load in all the children one at a time, and then add new ones as they are added to the items node in firebase:
itemsRef.observe(.childAdded) { snapshot in
// add a single child based on snapshot
items.append(Item(fromDict: snapshot.value as! [String:String])
}
That gets the job done, but seems hugely inefficient compared to using the getData() method they supply, which hands us a dictionary containing all the children:
itemsRef.getData() { error, snapshot in
// set all children at once base on snapshot
items = Item.itemArray(fromMetaDict: snapshot.value as! [String:[String:String]])
}
It would seem best to use getData() initially, then observe(.childAdded) to monitor additions. But then how do we prevent the observe completion block from running 1,000 times when it fires up? The firebase docs say that that's what will happen:
This event is triggered once for each existing child and then again every time a new child is added to the specified path.
Thanks in advance!
PS I didn't think it necessary to include definitions for the functions Item.init(fromDict:) or Item.itemArray(fromMetaDict:) — hopefully it's clear what they are meant to do.
There is (or: should be) no difference between listening for .childAdded on a path versus listening for .value or calling `getData() on that same path. The distinction is purely client-side and the wire traffic is (or: should be) the same.
It is in fact quite common to listen for .child* events to manipulate some data structure/UI, and then also listen for the .value event to commit those changes, as .value is guaranteed to fire after all corresponding .child*.
A common trick to do the first big batch of data in .value or getData(), and then use .child* for granular updates is to have a boolean flag to indicate whether you got the initial data already, and set if to false initially.
In the .child* handlers, only process the data if the flag is true.
itemsRef.observe(.childAdded) { snapshot in
if isInitialDataProcessed {
items.append(Item(fromDict: snapshot.value as! [String:String])
}
}
And then in your .value/getData handler, process the data and then set the flag to true.

Firestore: How to listen for changes to a document that doesn't exist yet?

I have two apps that access the same firestore database. The 'User' app listens for changes to a document in a collection that doesn't exist until the 'Admin' app creates it. Here is the code:
Stream<DocumentSnapshot>logStream = FirebaseFirestore.instance
.collection('companies')
.doc(companyId)
.collection('log')
.doc(DateFormat('yyyy-MM-dd').format(DateTime.now()))
.snapshots();
}
The problem is that I cannot start listening to 'logStream' unless it exists.
Is there a way to check when it's created and then start listening?
You can't start listening to stream immediatly after the document is created, that is not something that Firestore supports natively. So in order to do this you are going to have make a create a function that is called sporadically to check this until it is created, this function will make a get(), check if the document exists and if positive start listening to it.
You can use StreamCompleter, as mentioned in the comments, to listen to an "empty" stream before that without triggering any actions, but the calls to check existence of the document mentioned earlier will still need to be made, so you code can look like this:
var db = FirebaseFirestore.instance;
var completer = StreamCompleter<DocumentSnapshot>();
Stream<DocumentSnapshot>logStream = completer.stream();
// this function will need to be called sporadically
checkDocumentExists(completer);
checkDocumentExists(completer){
var reference = db.collection('companies')
.doc(companyId)
.collection('log')
.doc(DateFormat('yyyy-MM-dd').format(DateTime.now()));
reference.get() => then(function(document) {
if(document.exists){
completer.setSourceStream(reference.snapshots());
}
}
}
If you can't progress with your app execution until the document exists you can create an keep calling checkDocumentExists until the stream is ready.

Deleting, adding and updating Firestore documents when offline

I have been using the following code in my iOS Swift app:
class ProfileController
{
func remove(pid: String, completion: #escaping ErrorCompletionHandler)
{
guard let uid = self.uid else
{
completion(Errors.userIdentifierEmpty)
return
}
let db = Firestore.firestore()
let userDocument = db.collection("profiles").document(uid)
let collection = userDocument.collection("profiles")
let document = collection.document(pid)
document.delete()
{
error in
completion(error)
}
}
}
When the device is online, everything works fine. The completion handler of the deletecall is properly executed. However, when I am offline, I have noticed that the completion handler will not be executed as long as I am offline. As soon as I get back online, the completion handler will be called almost immediately.
I don't want to wait until the user is back online (which could take forever), so I changed the code a little bit and added a ListenerRegistration:
class ProfileController
{
func remove(pid: String, completion: #escaping ErrorCompletionHandler)
{
guard let uid = self.uid else
{
completion(Errors.userIdentifierEmpty)
return
}
let db = Firestore.firestore()
let userDocument = db.collection("profiles").document(uid)
let collection = userDocument.collection("profiles")
let document = collection.document(pid)
var listener: ListenerRegistration?
listener = document.addSnapshotListener(includeMetadataChanges: false)
{
snapshot, error in
listener?.remove() // Immediately remove the snapshot listener so we only receive one notification.
completion(error)
listener = nil
}
document.delete()
}
}
Although this works fine, I am not sure if this is the right way to go. I have read online that a snapshot listener can be used in real-time scenarios, which is not really what I am looking for (or what I need).
Is this the right approach or is there another (better) way? I only want to get notified once (thus I added the includeMetadataChanged property and set it to false). I also remove the ListenerRegistration once the completion handler was called once.
If the first approach does not work properly when being offline - what are the use cases of this approach? Before I change my entire codebase to use listeners, is there any way of executing the completion handler of the first approach when the device is offline?
TL;DR: The second implementation works fine, I am simply unsure if this is the proper way of receiving notifications when the device is offline.
If the first approach does not work properly when being offline - what are the use cases of this approach?
It depends on what you mean by "work properly". The behavior you're observing is exactly as intended. Write operations are not "complete" until they're registered at the server.
However, all writes (that are not transactions) are actually committed locally before they hit the server. These local changes will eventually be synchronized with the server at some point in the future, even if the app is killed and restarted. The change is not lost. You can count on the synchronization of the change to eventually hit the server as long as the user continues to launch the app - this is all you can expect. You can read more about offline persistence in the documentation.
If you need to know if a prior change was synchronized, there is no easy way to determine that if the app was killed and restarted. You could try to query for that data, if you know the IDs of the documents written, and you could check the metadata of the documents to find out the source of the data (cache or server). But in the end, you are really supposed to trust that changes will be synchronized with the server at the earliest convenience.
If your use case requires more granularity of information, please file a feature request with Firebase support.

Async/Await/then in Dart/Flutter

I have a flutter application where I am using the SQFLITE plugin to fetch data from SQLite DB. Here I am facing a weird problem. As per my understanding, we use either async/await or then() function for async programming.
Here I have a db.query() method which is conducting some SQL queries to fetch data from the DB. After this function fetches the data, we do some further processing in the .then() function. However, in this approach, I was facing some issues. From where I am calling this getExpensesByFundId(int fundId)function, it doesn't seem to fetch the data properly. It's supposed to return Future> object which will be then converted to List when the data is available. But when I call it doesn't work.
However, I just did some experimentation with it and added "await" keyword in front of the db.query() function and somehow it just started to work fine. Can you explain why adding the await keyword is solving this issue? I thought when using .then() function, we don't need to use the await keyword.
Here are my codes:
Future<List<Expense>> getExpensesByFundId(int fundId) async {
Database db = await database;
List<Expense> expenseList = List();
// The await in the below line is what I'm talking about
await db.query(expTable,where: '$expTable.$expFundId = $fundId')
.then((List<Map<String,dynamic>> expList){
expList.forEach((Map<String, dynamic> expMap){
expenseList.add(Expense.fromMap(expMap));
});
});
return expenseList;
}
In simple words:
await is meant to interrupt the process flow until the async method has finished.
then however does not interrupt the process flow (meaning the next instructions will be executed) but enables you to run code when the async method is finished.
In your example, you cannot achieve what you want when you use then because the code is not 'waiting' and the return statement is processed and thus returns an empty list.
When you add the await, you explicitly say: 'don't go further until my Future method is completed (namely the then part).
You could write your code as follows to achieve the same result using only await:
Future<List<Expense>> getExpensesByFundId(int fundId) async {
Database db = await database;
List<Expense> expenseList = List();
List<Map<String,dynamic>> expList = await db.query(expTable,where: '$expTable.$expFundId = $fundId');
expList.forEach((Map<String, dynamic> expMap) {
expenseList.add(Expense.fromMap(expMap));
});
return expenseList;
}
You could also choose to use only the then part, but you need to ensure that you call getExpensesByFundId properly afterwards:
Future<List<Expense>> getExpensesByFundId(int fundId) async {
Database db = await database;
List<Expense> expenseList = List();
return db.query(expTable,where: '$expTable.$expFundId = $fundId')
.then((List<Map<String,dynamic>> expList){
expList.forEach((Map<String, dynamic> expMap){
expenseList.add(Expense.fromMap(expMap));
});
});
}
// call either with an await
List<Expense> list = await getExpensesByFundId(1);
// or with a then (knowing that this will not interrupt the process flow and process the next instruction
getExpensesByFundId(1).then((List<Expense> l) { /*...*/ });
Adding to the above answers.
Flutter Application is said to be a step by step execution of code, but it's not like that.
There are a lot of events going to be triggered in the lifecycle of applications like Click Event, Timers, and all. There must be some code that should be running in the background thread.
How background work execute:
So there are two Queues
Microtask Queue
Event Queue
Microtask Queue runs the code which not supposed to be run by any event(click, timer, etc). It can contain both sync and async work.
Event Queue runs when any external click event occurs in the application like Click event, then that block execution done inside the event loop.
The below diagram will explain in detail how execution will proceed.
Note: At any given point of application development Microtask queue will run then only Event Queue will be able to run.
When making class use async for using await its simple logic to make a wait state in your function until your data is retrieve to show.
Example: 1) Its like when you follow click button 2) Data first store in database than Future function use to retrieve data 3) Move that data into variable and than show in screen 4) Variable show like increment in your following/profile.
And then is use one by one step of code, store data in variable and then move to next.
Example: If I click in follow button until data store in variable it continuously retrieve some data to store and not allow next function to run, and if one task is complete than move to another.
Same as your question i was also doing experiment in social media flutter app and this is my understanding. I hope this would help.
A Flutter question from an answer from your answer.
await is meant to interrupt the process flow until the async method has finished. then however does not interrupt the process flow but enables you to run code when the async method is finished. So, I am asking diff. between top down & bottom down process in programming.

CoreData, threading, main context and private context - works 99% of the time but

In my app I use two contexts: app delegate main context and a private context.
The private context is set as follows:
var privateContext: NSManagedObjectContext = NSManagedObjectContext(concurrencyType: NSManagedObjectContextConcurrencyType.PrivateQueueConcurrencyType)
privateContext.persistentStoreCoordinator = context.persistentStoreCoordinator
I also set an observer on the private context to trigger a save via main context:
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(MyView.managedObjectContextDidSave(_:)), name: NSManagedObjectContextDidSaveNotification, object: self.privateContext)
I trigger a data download with a callback to the current ViewController. During the download, I process all objects in:
privateContext.performBlock {
// process objects
....
// now save
if self.privateContext.hasChanges {
privateDataManager.save()
}
}
The save in the private context, triggers the observer and this code gets invoked:
dispatch_async(AppUtils.GlobalMainQueue, {
self.context.mergeChangesFromContextDidSaveNotification(notification)
})
The problem is that every now and then, not all changes get persisted. Cannot say when or why - when I debug it it always works...
How do I know I have a problem? Well, I compare object count before and after the download. To add more 'colour':
Every download of data adds some new records. The app then selects which records are out-of-date and marks them for deletion (sets a flag on a record). It then saves them (new and 'to be deleted') in the private context. The 'save' triggers a 'merge into primary context' notification.
In theory, this notification triggers the 'merge' in a synchronous manner.
After the merge, assuming it does happen in-order, there is data reload - this reload only loads records that do not have the 'deleted' flag set.
The problem I am having is that the 'merge' does not seem to always happen before I reload (that's the only way I can explain this).
My question, assuming my analysis is correct, is how to force the merge to happen before the reload? Does the notification not happen in a synchronous manner?
I guess I could always save the private context and instead of triggering the notification, simply create a notification object using private context and force trigger the merge. But I would like to understand why the above code does not work as expected.
The main source on which I based my code can be found here.