Flutter Firebase Database persistence not working - firebase-realtime-database

I am using the firebase_database plugin in version 1.0.1 with flutter currently testing on android.
I access the database with a singleton.
GlobalFBInstance._internal() {
final firebaseInstance = FirebaseDatabase.instance;
firebaseInstance.goOnline();
firebaseInstance.setPersistenceEnabled(true);
firebaseInstance.setPersistenceCacheSizeBytes(10000000);
databaseRef = firebaseInstance.reference();
databaseRef.keepSynced(true);
storageRef = FirebaseStorage.instance.ref();
}
Everytime after an app restart the app needs internet to get the database. I thought with the persistence and keepsynced there is no need for internet? If I have a very bad connection(tested in the emulator and on a device) it takes forever to load a gridview containing four simple strings from the database.
When I load a datasnapshot with:
Future<DataSnapshot> getDatabaseSnap(String location) async {
var _newref = databaseRef.child(location);
await _newref.keepSynced(true);
return await _newref.once();
}
it won't load if there the internet connection is slow.
What could be the reason for this? Is there a better way to make sure the database doesn't need a connection every time?
Thanks in advance.
Edit: When waiting for persistence I get false:
bool ispersistant = await firebaseInstance.setPersistenceEnabled(true);

Don't use FirebaseInstance to toggle persistent storage. Use FirebaseDatabase object to hold the instance and then set it to enable.
FirebaseDatabase database;
database = FirebaseDatabase.instance;
database.setPersistenceEnabled(true);
database.setPersistenceCacheSizeBytes(10000000); // 10MB cache is enough

Class Persistence Firebase database
class RealtimeDataBase{
late FirebaseDatabase firebaseDataBase;
late DatabaseReference databaseRef;
RealtimeDataBase(){
firebaseDataBase = FirebaseDatabase.instance;
firebaseDataBase.setPersistenceEnabled(true);
firebaseDataBase.setPersistenceCacheSizeBytes('specify any value');
databaseRef = firebaseDataBase.ref();
}
}

Related

NSFetchedResultsController not seeing inserts made by extension

I have an iOS app structured like this
Main Application (the main iOS app)
Intents Extension (Siri integration)
Shared Framework (shared library for interacting with Core Data. This allows both the main application and the intents extension to use the same Core Data store)
My issue is that when I insert something into Core Data using the Intents Extension, it doesn't appear in the Main Application's UITableView until I manually refresh the fetchedResultsController like this:
NSFetchedResultsController<NSFetchRequestResult>.deleteCache(withName: "myCache")
try? fetchedResultsController.performFetch()
tableView.reloadData()
Is there a way to make the fetchedResultsController see the changes without having to manually refresh everything?
Note: If I insert something into core data from the Main Application, the fetchedResultsController automatically sees the change and updates the table (like expected)
To share a database between an app and extension you need to implement Persistent History Tracking. For an introduction see WWDC 2017 What's New in Core Data at 20:49 and for sample code see the documentation Consuming Relevant Store Changes.
The basic idea is to enable the store option NSPersistentHistoryTrackingKey, observe NSPersistentStoreRemoteChangeNotification, upon being notified you should fetch the changes using NSPersistentHistoryChangeRequest and then merge into the context using mergeChangesFromContextDidSaveNotification and transaction.objectIDNotification. Your NSFetchedResultsController will then update accordingly.
This is normal because the application extension and the main application are not working in the same process.
There are some ways to update the data in the main application
NSPersistentStoreRemoteChangeNotification
UserDefaults(suitename:)
Darwin Notifications
I'm using UserDefaults and refreshAllObjects function for the viewContext.
Example:
func sceneDidBecomeActive(_ scene: UIScene) {
let defaults = UserDefaults(suiteName:"your app group name")
let hasChange = defaults?.bool(forKey: "changes")
if hasChange ?? false {
refreshAllObjects()
defaults?.set(false, forKey: "changes")
}
}
refresh all objects function is like this:
viewContext.perform {
viewContext.stalenessInterval = 0.0
viewContext.refreshAllObjects()
viewContext.stalenessInterval = -1
}

CoreStore how to observe changes in database

I need to observe changes of an Entity after import occurred.
Currently I have next logic:
Save Entity with temp identifier (NSManagedObject.objectId) to local core data storage.
Send Entity to the server via Alamofire POST request.
Server generates JSON and reply with the almost the same Entity details but with modified identifier which was NSManagedObject.objectId previously. So the local one Entity id will be updated with server id.
Now when I received new JSON I do transaction.importUniqueObjects.
At this step I want to inform my datasource about changes. And refetch data with updated identifiers.
So my DataSource has some Entities in an array, and while I use this datasource to show data it's still static information in that array which I fetched before, but as you see on the step number 4 I already updated core data storage via CoreStore import and want DataSource's array to be updated too.
I found some information regarding ListMonitor in CoreStore and tried to use it. As I can see this method works when update comes
func listMonitorDidChange(_ monitor: ListMonitor)
but I try to refetch data somehow. Looks like monitor already contains some most up to date info.
but when I do this:
func listMonitorDidChange(_ monitor: ListMonitor<MyEntity>) {
let entities = try? CoreStore.fetchAll(
From<MyEntity>()
.orderBy(.ascending(\.name))
) // THERE IS STILL old information in database, but monitor instance shows new info.
}
And then code became like this:
func listMonitorDidChange(_ monitor: ListMonitor<MyEntity>) {
var myEntitiesFromMonitor = [MyEntity]()
for index in 0...monitor.numberOfObjects() {
myEntitiesFromMonitor.append(monitor[index])
}
if myEntitiesFromMonitor.count > 0 {
// HERE we update DataSource
updateData(with: myEntitiesFromMonitor)
}
}
not sure if I am on the right way.
Please correct me if I am wrong:
As I understood each time core data gets updated with new changes, monitor gets updated as well. I have not dive deep into it how this was made, via some CoreData context notification or whatever but after you do something via CoreStore transaction, such as create or update or delete object or whatever you want, monitor gets update. Also it has callback functions that you need to implement in your class where you want to observe any changes with data model:
Your classes such as datasource or some service or even some view controller (if you don't use any MVVP or VIPER or other design patterns) need to conform to ListObserver protocol in case you want to listen not to just one object.
here are that functions:
func listMonitorDidChange(monitor: ListMonitor<MyPersonEntity>) {
// Here I reload my tableview and this monitor already has all needed info about sections and rows depend how you setup monitor.
// So you classVariableMonitor which I provide below already has up to date state after any changes with data.
}
func listMonitorDidRefetch(monitor: ListMonitor<MyPersonEntity>) {
// Not sure for which purposes it. I have not received this call yet
}
typealias ListEntityType = ExerciseEntity
let classVariableMonitor = CoreStore.monitorSectionedList(
From<ListEntityType>()
.sectionBy(#keyPath(ListEntityType.muscle.name)) { (sectionName) -> String? in
"\(String(describing: sectionName)) years old"
}
.orderBy(.ascending(\.name))
.where(
format: "%K == %#",
#keyPath(ListEntityType.name),
"Search string")
)
All other thing documented here so you can find info how to extract info from monitor in your tableview datasource function.
Thanks #MartinM for suggestion!

How to remove firebase database

Please tell me, how can I delete the entire database from firebase immediately, documentation or other information could not be found, can anyone come across this before?
it,s easy
db = Database.database().reference()
let usersReference = db
usersReference!.removeValue()
db = Database.database().reference()
let usersReference = db
usersReference!.removeValue()
Create an empty JSON file and import it to firebase database.

Firebase: How to persist user feed after iOS app is killed and restarted

I have a TableView in my app that loads a user's feed from the Firebase Database when the app is launched. Right now, if the user kills the app and reopens it, the TableView is completely blank and it calls the retrieveFeed function to retrieve the ten most recent posts again. I would like these posts to load in from the device locally rather than be called from the database server.
It's my understanding from reading the Firebase docs that this offline capability can be added by enabling persistence and setting keepSynced to true as I do in the code below. Do I need to use Core Data in order to keep the most recent ten posts in the feed in storage after the app is killed and restarted or should Firebase be capable of this?
In my AppDelegate.swift:
FirebaseApp.configure()
Database.database().isPersistenceEnabled = true
In my DataService.swift (which has all the calls to Firebase):
func retrieveFeed(queryNumber: UInt, lastSeenKey: String?, completion: #escaping ([ Int: DataService.FeedQuote ]) -> ()){
var dbquery = DatabaseQuery()
if (lastSeenKey == nil){
dbquery = self.user_ref.child(self.currentUserUserId).child("feed").queryOrderedByKey().queryLimited(toFirst: queryNumber)
}
else {
dbquery = self.user_ref.child(self.currentUserUserId).child("feed").queryOrderedByKey().queryStarting(atValue: lastSeenKey).queryLimited(toFirst: queryNumber)
}
dbquery.keepSynced(true)
dbquery.observeSingleEvent(of: .value, with: { (snapshot) in
var FeedQuotes = [DataService.FeedQuote]()
// ...
// Data processing here
// ...
completion(correctlyOrderedFeedQuotes)
})
}
I realized my error : Inside my function retrieveFeed I was also making calls to FirebaseStorage to retrieve images (which Firebase doesn't currently support caching of). These calls weren't completing properly with the persistence and syncing set up. Once I adjusted those calls using this pod (https://github.com/antonyharfield/FirebaseStorageCache), it started working!

NSKeyedArchiver and sharing a custom class between targets

My app uses a custom class as its data model:
class Drug: NSObject, NSCoding {
// Properties, methods etc...
}
I have just created a Today extension and need to access the user’s data from it, so I use NSCoding to persist my data in both the app container and the shared container. These are the save and load functions in the main app:
func saveDrugs() {
// Save to app container
let isSuccessfulSave = NSKeyedArchiver.archiveRootObject(drugs, toFile: Drug.ArchiveURL.path)
if isSuccessfulSave {
print("Drugs successfully saved locally")
} else {
print("Error saving drugs locally")
}
// Save to shared container for extension
let isSuccessfulSaveToSharedContainer = NSKeyedArchiver.archiveRootObject(drugs, toFile: Drug.SharedArchiveURL.path)
if isSuccessfulSaveToSharedContainer {
print("Drugs successfully saved to shared container")
} else {
print("Error saving drugs to shared container")
}
}
func loadDrugs() -> [Drug]? {
return NSKeyedUnarchiver.unarchiveObject(withFile: Drug.ArchiveURL.path) as? [Drug]
}
I encountered the problem of class namespacing where the NSKeyedUnarchiver in my Today extension could not decode the object properly, so I used this answer and added #objc before the class definition:
#objc(Drug)
class Drug: NSObject, NSCoding {
// Properties, methods etc...
}
This solved the problem perfectly. However, this will be version 1.3 of my app, and it seems this breaks the unarchiving process for pre-existing data (as I thought it might).
What is the best way to handle this scenario, as if I just make this change, the new version of the app will crash for existing users!
I cannot find any other answers about this, and I am not sure that the NSKeyedArchiver.setClass() method is relevant, nor am I sure where to use it.
Any help would be gratefully received. Thanks.
This is exactly the use case for NSKeyedUnarchiver.setClass(_:forClassName:) — you can migrate old classes forward by associating the current classes for their old names:
let unarchiver = NSKeyedUnarchiver(...)
unarchiver.setClass(Drug.self, forClassName: "myApp.Drug")
// unarchive as appropriate
Alternatively, you can provide a delegate conforming to NSKeyedUnarchiverDelegate and providing a unarchiver(_:cannotDecodeObjectOfClassName:originalClasses:), but that's likely overkill for this scenario.
Keep in mind that this works for migrating old data forward. If you have newer versions of the app that need to send data to old versions of the app, what you'll need to do is similar on the archive side — keep encoding the new class with the old name with NSKeyedArchiver.setClassName(_:for:):
let archiver = NSKeyedArchiver(...)
archiver.setClassName("myApp.Drug", for: Drug.self)
// archive as appropriate
If you have this backwards compatibility issue, then unfortunately, it's likely you'll need to keep using the old name as long as there are users of the old version of the app.

Resources