Firestore Offline Behaviour (Android, Kotlin)

March 1, 2019

Firestore Offline

  • Offline persistence is supported only in Android, iOS, and web apps.
  • For Android and iOS, offline persistence is enabled by default.
  • Querying works with offline persistence.

Add data: autogenarated id and callbacks

The above code below should not be used when offline, as addOnSuccessListener, addOnFailureListener and addOnCompleteListener shall not return as the backend could not be reached.

NOTE: The callbacks wouldn’t work for add/set/update when offline, but callbacks for get/query still works.

NOTE: The callbacks will returned when the device goes online and completed the backed commit. Imagine you have done 100 save calls with callbacks when offline, then you get 100 callbacks get triggered when the device goes online. Or you save call get triggered 1h later when you get online. If you do use callbacks, make sure you are ready to handle these scenarios.

Besides, we cannot access the autogenerated id as well.

val db = FirebaseFirestore.getInstance()

db.collection("test")
    .add(mapOf(
        "created" to FieldValue.serverTimestamp(),
        "posted_date" to Timestamp.now()
    ))
    .addOnSuccessListener {
        // will only run after committed to backend
        val id = it.id
    }
    .addOnFailureListener {
        // will only run after committed to backend
    }
    .addOnCompleteListener {
        // will only run after committed to backend
    }

The following code should be used, as we can now access the auto generated id.

val docRef = db.collection("test").document()
val id = docRef.id
Timber.d("id=$id")
docRef
    .set(mapOf(
        "created" to FieldValue.serverTimestamp(),
        "posted_date" to Timestamp.now(),
        "name" to "test"
    ))
id=Xi3exHiSGViSi1SAQv4v

NOTE: Notice auto generated id works offline (without access to backend).

We can use the docRef to perform a query.

docRef.get().addOnSuccessListener { document ->
    val id = document.id
    val created = (document["created"] as? Timestamp)?.toDate()
    val postedDate = (document["posted_date"] as? Timestamp)?.toDate()
    Timber.d("id=$id, created=$created, postedDate=$postedDate")
}
id=Xi3exHiSGViSi1SAQv4v, created=null, postedDate=Fri Mar 01 17:37:11 GMT+08:00 2019

NOTE: Notice that created (FieldValue.serverTimestamp()) is null, as backend server could not be reached.

You can use id to perform a query as well.

db.collection("test")
    .document(id)
    .get()
    .addOnSuccessListener { document ->
        val id = document.id
        val created = (document["created"] as? Timestamp)?.toDate()
        val postedDate = (document["posted_date"] as? Timestamp)?.toDate()
        Timber.d("id=$id, created=$created, postedDate=$postedDate")
    }
    .addOnFailureListener {
        Timber.w(it)
    }

Server timestamp and query order

We shall perform a batch insert of documents using both server timestamp and local timestamp as well.

val batch = db.batch()
for (i in 1..10) {
    val docRef = db.collection(Test.COLLECTION_NAME).document()
    batch.set(docRef, mapOf(
        "created" to FieldValue.serverTimestamp(),
        "posted_date" to Timestamp.now(),
        "test_offline_order" to true,
        "sequence" to i
    ))
}
batch.commit()

Query order by created wouldn’t work when offline (as it required a server to fill these values), so we have to query by posted_date which used a local time.

db.collection("test")
    .whereEqualTo("test_offline_order", true)
    // .orderBy("created", Query.Direction.DESCENDING)
    .orderBy("posted_date", Query.Direction.DESCENDING)
    .get()
    .addOnSuccessListener { documents ->
        Timber.d("get: success")
        for (document in documents) {
            val created = (document["created"] as? Timestamp)?.toLocalDateTime()
            val postedDate = (document["posted_date"] as? Timestamp)?.toLocalDateTime()
            val sequence = document["sequence"] as Long
            Timber.d("id=${document.id}, created=$created, posted=$postedDate, sequence=$sequence")
        }
    }

NOTE: Refer to Timestamp.toLocalDateTime() Kotlin extension

.orderBy("created", Query.Direction.DESCENDING)

id=bvtL4yk3IaFvPGHp6V6v, created=null, posted=2019-03-01T16:18:21.896, sequence=4
id=agWieap4Xi95qnn4fYk5, created=null, posted=2019-03-01T16:18:21.895, sequence=3
id=Vw1XLwFB0cXOhbTkJiSg, created=null, posted=2019-03-01T16:18:21.911, sequence=8
id=So3ePFXP5nMAaKbAq4f1, created=null, posted=2019-03-01T16:18:21.894, sequence=2
id=NhnlAl7XPicINfzR1i85, created=null, posted=2019-03-01T16:18:21.915, sequence=10
id=KfOn2kS9dpNpMzr5hQ0S, created=null, posted=2019-03-01T16:18:21.913, sequence=9
id=Gyxnw0vD4dTuCYM8gOTs, created=null, posted=2019-03-01T16:18:21.906, sequence=6
id=FNxphM50s4yyyPsQccvw, created=null, posted=2019-03-01T16:18:21.855, sequence=1
id=5pSFOxspzl75hYZzjnut, created=null, posted=2019-03-01T16:18:21.898, sequence=5
id=4eSKFhSK8wvapwiz71TP, created=null, posted=2019-03-01T16:18:21.909, sequence=7

.orderBy("posted_date", Query.Direction.DESCENDING)

id=NhnlAl7XPicINfzR1i85, created=null, posted=2019-03-01T16:18:21.915, sequence=10
id=KfOn2kS9dpNpMzr5hQ0S, created=null, posted=2019-03-01T16:18:21.913, sequence=9
id=Vw1XLwFB0cXOhbTkJiSg, created=null, posted=2019-03-01T16:18:21.911, sequence=8
id=4eSKFhSK8wvapwiz71TP, created=null, posted=2019-03-01T16:18:21.909, sequence=7
id=Gyxnw0vD4dTuCYM8gOTs, created=null, posted=2019-03-01T16:18:21.906, sequence=6
id=5pSFOxspzl75hYZzjnut, created=null, posted=2019-03-01T16:18:21.898, sequence=5
id=bvtL4yk3IaFvPGHp6V6v, created=null, posted=2019-03-01T16:18:21.896, sequence=4
id=agWieap4Xi95qnn4fYk5, created=null, posted=2019-03-01T16:18:21.895, sequence=3
id=So3ePFXP5nMAaKbAq4f1, created=null, posted=2019-03-01T16:18:21.894, sequence=2
id=FNxphM50s4yyyPsQccvw, created=null, posted=2019-03-01T16:18:21.855, sequence=1
This work is licensed under a
Creative Commons Attribution-NonCommercial 4.0 International License.