Оффлайн проблема с Firestore vs Firebase

Я конвертировал одно из моих приложений в новый Firestore. Я делаю такие вещи, как сохранение документа по нажатию кнопки, а затем в onSuccess слушатель, идущий на другую деятельность.

Я также использую тот факт, что операции сохранения Firestore возвращают задачи, чтобы группировать задачи вместе, используя Tasks.whenAll:

val allTasks = Tasks.whenAll(
       createSupporter(supporter),,
       setStreetLookup(makeStreetKey(supporter.street_name)),
       updateCircleChartForUser(statusChange, createMode = true), 
       updateStatusCountForUser(statusChange))

      allTasks.addOnSuccessListener(this@SignUpActivity, successListener)
      allTasks.addOnFailureListener(this@SignUpActivity, onFailureListener)

Наконец, я получаю идентификатор документа из успешного сохранения и сохраняю его в настройках или в локальной базе данных для последующего использования (в пределах onSuccessListener)

Это все прекрасно работает. Пока не произойдет потеря сетевого подключения. Тогда все рушится, потому что задачи никогда не завершаются, и слушатели onSuccess / onFailure / onComplete никогда не вызываются. Так что приложение просто зависает.

Я работаю над этим, проверяя доступность сети перед каждым сохранением, а затем выполняю обходные пути, создавая задачи без каких-либо слушателей. Я также генерирую идентификатор документа локально с помощью генератора UUID.

Кстати, это было не то, как приложение работало со старой базой данных. В этом случае все работало хорошо в автономном режиме, и я видел, как документы синхронизировались всякий раз, когда приложение выходило в сеть.

Мой обход Firestore кажется ужасным взломом. Кто-нибудь придумал лучшее решение?

См. Связанную базу данных Firestore о том, что обратные вызовы вставки / удаления документов не вызываются, когда нет соединения addOnCompleteListener, не вызываемого в автономном режиме с облачным firestore

5 ответов

Cloud Firestore предоставляет нам возможность обрабатывать автономные данные, но вам нужно использовать "Снимок" (QuerySnapshot, DocumentSnapshot), чтобы обработать этот случай, к сожалению, он плохо документирован. Вот пример кода (я использую Kotlin Android) для обработки случая с помощью Snapshot:

ОБНОВЛЕНИЕ ДАННЫХ:

db.collection("members").document(id)
  .addSnapshotListener(object : EventListener<DocumentSnapshot> {
      override fun onEvent(snapshot: DocumentSnapshot?,
                           e: FirebaseFirestoreException?) {
          if (e != null) {
              Log.w(ContentValues.TAG, "Listen error", e)
              err_msg.text = e.message
              err_msg.visibility = View.VISIBLE;
              return
          }
          snapshot?.reference?.update(data)

      }
  })

ДОБАВИТЬ ДАННЫЕ:

db.collection("members").document()
 .addSnapshotListener(object : EventListener<DocumentSnapshot> {
     override fun onEvent(snapshot: DocumentSnapshot?,
                          e: FirebaseFirestoreException?) {
         if (e != null) {
             Log.w(ContentValues.TAG, "Listen error", e)
             err_msg.text = e.message
             err_msg.visibility = View.VISIBLE;
             return
         }
         snapshot?.reference?.set(data)

     }
 })

УДАЛИТЬ ДАННЫЕ:

db.collection("members").document(list_member[position].id)
   .addSnapshotListener(object : EventListener<DocumentSnapshot> {
       override fun onEvent(snapshot: DocumentSnapshot?,
                            e: FirebaseFirestoreException?) {
           if (e != null) {
               Log.w(ContentValues.TAG, "Listen error", e)
               return
           }
           snapshot?.reference?.delete()
       }
   })

Вы можете увидеть пример кода здесь: https://github.com/sabithuraira/KotlinFirestore и сообщение в блоге http://blog.farifam.com/2017/11/28/android-kotlin-management-offline-firestore-data-automatically-sync-it/

В случае потери сетевого подключения (нет сетевого подключения на пользовательском устройстве), ни onSuccess() ни onFailure() срабатывают. Такое поведение имеет смысл, поскольку задача считается выполненной только тогда, когда данные были зафиксированы (или отклонены) на сервере Firebase. onComplete(Task<T> task) Метод вызывается также только после завершения Задачи. Так что в случае отсутствия подключения к Интернету, ни onComplete срабатывает.

Нет необходимости проверять доступность сети перед каждым сохранением. Существует обходной путь, который легко может помочь вам понять, действительно ли клиент Firestore не может подключиться к серверу Firebase, который enabling debug logging:

FirebaseFirestore.setLoggingEnabled(true);

Операции, которые записывают данные в базу данных Firestore, определены для signal completion как только они на самом деле преданы бэкэнду. В результате это работает как задумано: в автономном режиме они не будут сигнализировать о завершении.

Обратите внимание, что клиенты Firestore внутренне гарантируют, что вы можете читать свои собственные записи, даже если вы не ждете завершения задачи от удаления.

Для офлайн-поддержки вам необходимо установить Source.CACHE

docRef.get(Source.CACHE).addOnCompleteListener(new OnCompleteListener<DocumentSnapshot>() {
    @Override
    public void onComplete(@NonNull Task<DocumentSnapshot> task) {
        if (task.isSuccessful()) {
            // Document found in the offline cache
            DocumentSnapshot document = task.getResult();

        } else {
            //error
        }
    }
});

Я узнал, как это сделать, используя информацию на http://blog.farifam.com/. В основном вы должны использовать SnapshotListeners вместо OnSuccess слушатели для оффлайн-работы. Кроме того, вы не можете использовать задачи Google, потому что они не будут конкурировать в автономном режиме.

Вместо этого (поскольку задачи в основном являются обещаниями), я использовал библиотеку Kotlin Kovenant, которая может прикреплять слушателей к обещаниям. Одно из предупреждений заключается в том, что вы должны настроить Kovenant для разрешения многократного разрешения для обещания, поскольку прослушиватель событий может вызываться дважды (один раз, когда данные добавляются в локальный кэш, и один раз, когда они синхронизируются с сервером).

Вот пример фрагмента кода с прослушивателями успеха / неудачи, который работает как в сети, так и в автономном режиме.

val deferred = deferred<DocumentSnapshot, Exception>() // create a deferred, which holds a promise
// add listeners
deferred.promise.success { Log.v(TAG, "Success! docid=" + it.id) }
deferred.promise.fail { Log.v(TAG, "Sorry, no workie.") }

val executor: Executor = Executors.newSingleThreadExecutor()
val docRef = FF.getInstance().collection("mydata").document("12345")
val data = mapOf("mykey" to "some string")

docRef.addSnapshotListener(executor, EventListener<DocumentSnapshot> { snap: DocumentSnapshot?, e: FirebaseFirestoreException? ->
    val result = if (e == null) Result.of(snap) else Result.error(e)
    result.failure {
        deferred.reject(it) // reject promise, will fire listener
    }
    result.success { snapshot ->
        snapshot.reference.set(data)
        deferred.resolve(snapshot) // resolve promise, will fire listener
    }
})

Этот код на языке Dart, так как я использую его во Flutter, но вы легко можете изменить его на свою платформу и язык.

      Future<void> updateDoc(String docPath, Map<String, dynamic> doc) async {
    doc["updatedAt"] = Utils().getCurrentTimestamp();
    DocumentReference documentReference = _firestore.doc(docPath);

    Completer completer = Completer();
    StreamSubscription streamSubscription;
    streamSubscription = documentReference
        .snapshots(includeMetadataChanges: true)
        .listen((DocumentSnapshot updatedDoc) {
      // Since includeMetadataChanges is true this will stream new data as soon as
      // it update in local cache so it data has same updateAt it means it new data
      if (updatedDoc.data()["updatedAt"] == doc["updatedAt"]) {
        completer.complete();
        streamSubscription.cancel();
      }
    });
    documentReference.update(doc);
    return completer.future;
  }

Так как includeMetadataChangesустановлен, он будет отправлять данные в поток, когда локальный кеш изменится на поэтому, когда вы вызываете обновление, вы получите данные, как только локальный кеш обновится. Вы можете использовать Completerчтобы завершить свое будущее. Теперь ваш метод ждет только обновления локального кеша, и вы можете использовать awaitза updateDoc

Другие вопросы по тегам