Question about iOS Firebase offline writes and callback behavior

186 views
Skip to first unread message

erot...@gmail.com

unread,
Aug 20, 2020, 4:09:44 AM8/20/20
to Firebase Google Group

Hi, I recognize that questions around mine have come up on numerous occasions and on numerous forums.  I've looked through SO, here, the Slack Firebase group, and the Firebase docs, and I have yet to see a definitive response (save one on here from a while ago that I'll link to later).  I'd really appreciate a clarification and/or advice on whether I have something set up incorrectly.

For background, as I said, I'm using Firebase on iOS.  I've also set it up for offline persistence via the `Database.database().isPersistenceEnabled = true` command, which is called immediately after I call `FirebaseApp.configure()`.  I'm also using Firebase as the sole source of truth (all updates and data come from Firebase), which I would think would be normal use.  What I'm seeing is that none of the completion callbacks for `setValue()` or `updateChildValues()` are being called while the app is offline; they're only called after the app regains connectivity.  In addition, the `observer()` listener callbacks I have set up for my various references are only called (`eventType` is `value`) when the app regains connectivity; not when it's offline.

From the docs: "By enabling persistence, any data that the Firebase Realtime Database client would sync while online persists to disk and is available offline, even when the user or operating system restarts the app. This means your app works as it would online by using the local data stored in the cache. Listener callbacks will continue to fire for local updates."

From this, it would seem that if I perform a write while the app is offline my `observe()` callback references should fire: "Listener callbacks will continue to fire for local updates." From https://groups.google.com/g/firebase-talk/c/59aOusqAMEQ/m/N9r9lLrGAAAJ, this also seems to be the case: "Your local app will still get added/updated/removed events..."

I'd appreciate it if someone could clarify if I'm doing something incorrectly and/or what expected behavior is in this situation.  If you need any more information, please let me know.  Thanks!

Frank van Puffelen

unread,
Aug 20, 2020, 1:20:40 PM8/20/20
to Firebase Google Group
Hey there,

Thanks for the detailed question.

Completion listeners on write operations only fire when those writes are committed on the server. So the fact that you don't see them firing while you're offline is expected. If you want to know when the local write has complete, it's actually much simpler: when the setValue() or updateChildValues() call is done, the local write operation is completed - so the write is reflected in the local cache, and (while offline) the pending write is queued for sending to the server.

Local events should fire normally during this time, but a lot depends on precisely what node you listen to and what node you write. The main reason for this is that the SDK guarantees it will never fire a snapshot with partial data. So if you write to /x/y/z and listen to /x/y, the client will only fire local events if it has a full snapshot of `/x/y` in cache already (that it then updated with your local write).

I hope this explains what you're seeing. If not, can you create a small standalone repro so that I can try it myself?

    puf

Kato Richardson

unread,
Aug 20, 2020, 1:28:25 PM8/20/20
to Firebase Google Group
Note that you could add keepSynced() on /x/y in Puf's example, in order to ensure you have a complete local copy (be careful with using this on substantially large nodes of course).

☼, Kato

--
You received this message because you are subscribed to the Google Groups "Firebase Google Group" group.
To unsubscribe from this group and stop receiving emails from it, send an email to firebase-tal...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/firebase-talk/f270664a-3cf0-45b9-9cf1-fbc64cefcbd6o%40googlegroups.com.


--

Kato Richardson | Developer Programs Eng | kato...@google.com | 775-235-8398

erot...@gmail.com

unread,
Aug 21, 2020, 11:14:58 AM8/21/20
to Firebase Google Group
Thanks to both of you for the replies and info; it's been helpful.  Let me give an outline of how I'm structuring my database and perhaps you can advise further.  I've tried to follow the best practices docs and have set up the database to be pretty flat; i.e., if x is a model object that has numerous y model objects that it owns, then I've represented that in the database as follows:
{ "<x's_path_name>":
        "<x1_UUID>": {
            "<x1_name>": ".....",
            "<y's_path_name>: {
                "<y1_UUID>" : true,
                "<y2>UUID>": true,
                ....
            }
            ...
        },
  "<y's_path_name>":
      "<y1_UUID>": {
          "<y1_name": "...."
        }
    }
}

After the user authenticates, the app would show all the x objects in a tableview and clicking on any x object would then show another tableview with all its associated y objects.  Given that there doesn't seem to be a way to do a batch query of a node for a list of UUIDs contained in it (i.e., "return all y children at <y's_path_name> with matching UUIDs from <x's_path_name>/<x1_UUID>/<y's_path_name>"), I attach listeners individually to each y UUID found in <x1_UUID>/<y's_path_name> when I want to return all y objects associated with x when the user would tap on a tableview cell that represents x.  I believe I read something on a SO discussion from possibly Frank that this was the recommended way to do these types of bulk read operations.  In my actual use case, this structure is much more "nested," in that y would have z objects set up in a similar fashion to how x has y objects set up, and there's another level down.  I thought of attaching a listener that would fire anytime there was a change to <y's_path_name> dictionary, but it will have loads of y objects belonging to various users and I don't think it'd be practical to fire off an observer anytime there's a change—would require tons of local filtering operations.

After going back and looking at the existing app behavior further, I'm seeing that the offline behavior you say should be happening is partially occurring: It's working for calls to `updateChildValues()`.  I call this to perform deletions and edits to x and y.  When I do this, my `observe()` handler fires for that reference and when connectivity is regained the completion handler for `updateChildValues()` fires—sounds like expected offline behavior from what you said.

However, what's not working when the app is offline is multi-fold:
1) If do a clean install and am on the tableview displaying x objects, the app goes offline, and a tableview cell is selected, nothing is retrieved for all its associated y objects.  This makes sense, as I haven't attached listeners to the associated y objects before connectivity is lost—Kato mentions about a complete local copy of the database not being made by default (understandably).
2) However, if I go back online while sitting on that same empty y object view, force-quit the app after those y objects have been loaded successfully (they're loaded as soon as connectivity is regained), come back into the app, go offline after the x objects have loaded, and then tap on the same x cell whose y objects had already been loaded, none of those y object observers fire.
3) No calls to `setValue()` when the app is offline are causing x's reference `observer()` to fire.  I use this method to add new objects to the database.  So, what I'd do to add a new y3 object to x1 would be to first call `setValue()` on <y's_path_name> with all of y3's data, then call `setValue()` on <x's_path_name>/<x1_UUID>/<y's_path_name> and pass it a dictionary of { "<y3_UUID>": true }.

So, given this lengthy response (sorry in advance for it!), what do you recommend? To fix #1 above, I could load my entire object graph for a given user's x and y objects when the app starts but that doesn't sound ideal.  I could also call `keepSynced()` on individual refs, as Kato said—think this could solve #2.  Not sure what I can do to solve #3.

Thanks,
Evan

Frank van Puffelen

unread,
Aug 21, 2020, 7:42:35 PM8/21/20
to Firebase Google Group
Hey Evan,

I'm mostly surprised about your #2. I expect you to load the individual Y objects there, which should be available in the cache in a state that can be returned. Can you create a single snippet that I (or someone on our side) can run in an iOS app to reproduce just this problem?

    puf

erot...@gmail.com

unread,
Aug 23, 2020, 12:15:51 PM8/23/20
to Firebase Google Group
Frank,

Hi, ok, I'll work on putting something simple together to reproduce #2.  In the meantime, do you have any recommendations to resolve #3? This is the issue that's most troubling for me.

Thanks,
Evan

erot...@gmail.com

unread,
Aug 24, 2020, 12:00:17 AM8/24/20
to Firebase Google Group
Actually, I was able to address all my issues in the process of putting together a small project for you.  I need to look into what I'm doing in my app in more detail, but it's possible adding offline records may not have been working because I was chaining the call to `observe()` off the `setValue()` callback.  I'm also using RxSwift for all the Firebase operations so it's possible I've set up the observable chain incorrectly.

Thanks for your help,
Evan

Frank van Puffelen

unread,
Aug 24, 2020, 12:04:01 AM8/24/20
to Firebase Google Group
Good to hear Evan. Let me know if you get the repro working after all, or if you figure out for certain what the problem was in your original code.

erot...@gmail.com

unread,
Aug 24, 2020, 11:55:11 PM8/24/20
to Firebase Google Group
Frank,

Hi, I will for sure! One more question, if you have a moment.  What is the better practice around querying for a user's data: Make the database very flat by placing all x items in one dictionary and attaching individual listeners to each x item that belongs to a user (this is what I've done), or have a sub-dictionary under x for all x items that belong to a given user (key could be user UUID) and attach one listener to that whole dictionary? It's basically something like:

// Retrieve list of all x items for a given user first, then attach listeners to each item, since x list is for all users
{ "x_list":
    { "x_instance_uuid":
        { "name": ....,
           "x_instance_uuid": ...
        },
        ...
    },
    ...
}

// Create sub-directories of x with each user UUID as a key and attach a listener to each sub-dictionary
{ "x_list":
    { "user_1_uuid":
        { "x_instance_uuid":
            { "name": ....,
               "x_instance_uuid": ...
            }
        },
        ...
    },
    ...
}

Thanks,
Evan

Frank van Puffelen

unread,
Aug 25, 2020, 7:55:55 PM8/25/20
to Firebase Google Group
As usual... it depends.

But the common concerns:
  • Attaching multiple once() listeners is not as slow as many developers initially think, as Firebase pipelines all requests over a single connection.
  • Duplicating data to prevent multiple lookups is a common practice on NoSQL database, no matter what your "used to BCNF" minds may keep telling us.
best,

    puf

erot...@gmail.com

unread,
Aug 26, 2020, 4:49:34 AM8/26/20
to Firebase Google Group
Thanks for that; think I'll stick with my existing approach to using multiple `once()` listeners.  Figured out what was going on in my app! To give you some background, I have a service layer for doing all the Firebase operations (so it's all handled in one place) and am using RxSwift.  On Firebase record creation, I call `setValue()` twice for updating data in two locations.  The second write triggers an update via an `observe()` at that location, as I have an ongoing (RxSwift) subscription to the location being written to.  What was happening is that I was chaining the calls, so when the app was offline it wouldn't return from the first call until connectivity was regained.  This meant that the second call to `setValue()` (the one that kicks off the `observe()` updates) wasn't happening until later.  This is why I'd only get updates once connectivity was regained.  This jibes with other discussions I've seen around not relying on `setValue()` and friends to complete before performing other actions.

Thanks again for the help!
Reply all
Reply to author
Forward
0 new messages