I converted one of my apps to the new Firestore. I am doing things like saving a document on a button click, and then in the onSuccess listener, going to a different activity.
I also use the fact that Firestore save operations return tasks, to group tasks together using 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)
Finally, I get the document id from a successful save and store it in preferences or in a local database for later use (within the onSuccessListener)
This all works great. Until there is a loss of network connectivity. Then everything falls apart, because the tasks never complete and the onSuccess/onFailure/onComplete listeners never get called. So the app just hangs.
I am working around this by checking for network availability before each save, and then doing a work-around by creating tasks without any listeners. I am also generating a document id locally using a UUID generator.
This, BTW, was not the way the app worked with the old firebase. In that case, everything ran nicely when offline and I saw documents getting synced up whenever the app came online.
My workaround for Firestore seems a terrible hack. Has anyone come up with a better solution?
See related Firestore database on insert/delete document callbacks not being invoked when there is no connection
addOnCompleteListener not called offline with cloud firestore
Cloud Firestore provide us feature for handle offline data but you need to use “Snapshot” (QuerySnapshot, DocumentSnapshot) to handle this case, unfortunately it not documented well. This is some code example (I use Kotlin Android) to handle case using Snapshot:
UPDATE DATA:
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)
}
})
ADD 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)
}
})
DELETE 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()
}
})
You can see code example here: https://github.com/sabithuraira/KotlinFirestore and blog post http://blog.farifam.com/2017/11/28/android-kotlin-management-offline-firestore-data-automatically-sync-it/
When there is a loss of network connectivity (there is no network connection on user device), neither onSuccess() nor onFailure() are triggered. This behavior makes sense, since the task is considered completed only when the data has been committed (or rejected) on the Firebase server. So onSuccess() will fire only when the task completes successfully.
There is no need to check for network availability before each save. There is a workaround that easily can help you see if the Firestore client indeed can't connect to the Firebase server, which is by enabling debug logging:
FirebaseFirestore.setLoggingEnabled(true);
Operations that write data to the Firestore database are defined to signal completion once they've actually committed to the backend. As a result, this is working as intended: while offline they won't signal completion.
Note that the Firestore clients internally guarantee that you can read your own writes even if you don't wait for the completion of the task from delete. The Firestore client is designed to continue functioning fine without an internet connection. So writing/deleting to the database without an internet connection is (by design) possible, and will never yield an error.
I found out how to do it using info at http://blog.farifam.com.
Basically you must use SnapshotListeners instead of OnSuccess listeners for offline work.
Also, you cannot use Google's tasks because they won't compete offline.
Instead (since Tasks are basically Promises), I used the Kotlin Kovenant library which can attach listeners to promises. One wrinke is that you must configure Kovenant to allow multiple resolution for a promise, since the event listener can be called twice (once when the data is added to the local cache, and once when it is synced to the server).
Here is an example snippet of code, with success/failure listeners, that runs both online and offline.
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
}
})
For offline support you need to set 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
}
}
});
This code in Dart language since I use in Flutter but you easily can change it to your platform and language
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;
}
So since includeMetadataChanges is set it will send data to stream when local cache changes to so when you call update you will receive data as soon as local cache update. You can use Completer to complete your future. Now your method only wait to update local cache and you can use await for updateDoc
Related
I have a DAO class where I have fetchHubList method which fetches a collection of documents from cloud Firestore asynchronously using await(). This implementation used the "get()" method which I got to know later on does not fetch real-time updates. On trying to implement the code similarly using onSnapshotListener gives an error (which was quite expected to be honest, because get() and this methods return quite different things). Does anyone have any idea how to implement this?
How the code is currently:
suspend fun fetchHubList(): ArrayList<HubModel>? = try {
val hubList = ArrayList<HubModel>()
hubsListCollection.get().await().map { document ->
if (document != null) {
Log.d(TAG, "Data fetch successful!")
Log.d(TAG, "the document id is ${document.id}")
val temp = HubModel(document.get("hubName").toString(),
document.id.toString(),
document.get("isAdmin") as Boolean)
hubList.add(temp)
// hubList.add(document.toObject(HubModel::class.java))
} else {
Log.d(TAG, "No such document")
}
}
And what I want to implement here (and which is totally erroneous):
suspend fun fetchHubList(): ArrayList<HubModel>? = try {
val hubList = ArrayList<HubModel>()
hubsListCollection.addSnapshotListener().await().map { document ->
if (document != null) {
Log.d(TAG, "Data fetch successful!")
Log.d(TAG, "the document id is ${document.id}")
val temp = HubModel(document.get("hubName").toString(),
document.id.toString(),
document.get("isAdmin") as Boolean)
hubList.add(temp)
// hubList.add(document.toObject(HubModel::class.java))
} else {
Log.d(TAG, "No such document")
}
}
I use this function in my ViewModel class to create a LiveData wrapped ArrayList:
val hubList = MutableLiveData<ArrayList<HubModel>>()
private val hubListDao = HubListDao()
init {
viewModelScope.launch {
hubList.value = hubListDao.fetchHubList()
}
}
Thanks in advance!
You don't need addSnapshotListener, just use get:
hubsListCollection.get().await()
In order to observe changes in your collection you can extend LiveData:
class CafeLiveData(
private val documentReference: DocumentReference
) : LiveData<Cafe>(), EventListener<DocumentSnapshot> {
private var snapshotListener: ListenerRegistration? = null
override fun onActive() {
super.onActive()
snapshotListener = documentReference.addSnapshotListener(this)
}
override fun onInactive() {
super.onInactive()
snapshotListener?.remove()
}
override fun onEvent(result: DocumentSnapshot?, error: FirebaseFirestoreException?) {
val item = result?.let { document ->
document.toObject(Cafe::class.java)
}
value = item!!
}
}
And expose it from your view model:
fun getCafe(id: String): LiveData<Cafe> {
val query = Firebase.firestore.document("cafe/$id")
return CafeLiveData(query)
}
As #FrankvanPuffelen already mentioned in his comment, there is no way you can use ".await()" along with "addSnapshotListener()", as both are two totally different concepts. One is used to get data only once, while the second one is used to listen to real-time updates. This means that you can receive a continuous flow of data from the reference you are listening to.
Please notice that ".await()" is used in Kotlin with suspend functions. This means that when you call ".await()", you start a separate coroutine, which is a different thread that can work in parallel with other coroutines if needed. This is called async programming because ".await()" starts the coroutine execution and waits for its finish. In other words, you can use ".await()" on a deferred value to get its eventual result, if no Exception is thrown. Unfortunately, this mechanism doesn't work with real-time updates.
When it comes to Firestore, you can call ".await()" on a DocumentReference object, on a Query object, or on a CollectionReference object, which is actually a Query without filters. This means that you are waiting for the result/results to be available. So you can get a document or multiple documents from such calls. However, the following call:
hubsListCollection.addSnapshotListener().await()
Won't work, as "addSnapshotListener()" method returns a ListenerRegistration object.
I want to use a snapshot listener to listen to changes that might occur in my database to update my RecyclerView
In this case, you should consider using a library called Firebase-UI for Android. In this case, all the heavy work will be done behind the scenes. So there is no need for any coroutine or ".await()" calls, everything is synched in real-time.
If you don't want to use either Kotlin Coroutines, nor Firebase-UI Library, you can use LiveData. A concrete example can be seen in my following repo:
https://github.com/alexmamo/FirestoreRealtimePagination/blob/master/app/src/main/java/ro/alexmamo/firestorerealtimepagination/ProductListLiveData.java
Where you can subclass LiveData class and implement EventListener the interface.
I am building a client application which uses Firebase for two things:
User Authentication
Using a realtime database
I have managed to set up everything correctly on my client and on my backend server (using Firebase's Admin SDK) and am able to correctly authenticate users and allow them to read/write to the database.
I am also using Retrofit2 to send requests from the client to the backend.
As part of allowing users access to the database, it is needed to send the user's token to the backend so the user can be verified.
To do this, I have the following logic:
val user = FirebaseAuth.getInstance().currentUser
if (user != null) {
user.getIdToken(false).addOnCompleteListener {
if (it.isSuccessful) {
val token = it.result?.token
//retrofit logic to send request happens from here
}
}
As you can see, getting the Id token of the user is an asynchronous call and in the current code base that I have, I have this code block for each one of my calls to the backend (duplication).
I want to know how I can export this snippet to a function (maybe a suspend method?) so that it can be reused for every call to the backend
I have searched online and have seen many SO questions, but none that fit this scenario.
I have thought about passing in a callback, but I have several methods that communicate to the backend, and each of them will require a different callback method.
The solution I am looking for looks something like this:
fun fetchDataFromDB() {
getIdTokenForUser()
//wait till it finishes and then
//perform request to DB
}
fun updateDataInDB() {
getIdTokenForUser()
//wait till it finishes and then
//perform request to DB
}
//......
I have tried reading about and implementing coroutines, but I lack the knowledge to do so correctly.
EDIT
Thanks to #Doug Stevenson for his answer and direction, I have managed to construct the following:
private suspend fun getUserIdToken(user: FirebaseUser) = coroutineScope {
val job = async {
user.getIdToken(false).result?.token
}
job.await()
}
And I use it in this fashion:
fun updateDB(context: Context) = runBlocking {
val user = FirebaseAuth.getInstance().currentUser
if (user != null) {
val token = getUserIdToken(user)
}
}
Is this the correct approach? Since the answers given below present a different implementation.
getIdToken is asynchronous returns a Task object. If you want to use a Task object in a Kotlin coroutine, you can use the library kotlinx-coroutines-play-services to add an extension method await() to the Task that makes it usable in a coroutine. With that, you can write something like this:
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.3.9"
import kotlinx.coroutines.tasks.await
suspend fun getIdTokenForUser(user: FirebaseUser): GetTokenResult {
return try {
user.getIdToken(false).await()
}
catch (e: Exception) {
// handle error
}
}
You might have to update the types here - I didn't try to compile or test this.
See also:
Android kotlin task to be executed using coroutines
Coroutines And Firebase: How to Implement Javascript-like Promise.all()
Using Firebase with Kotlin coroutines
In order to go from a callback based API like the following one:
val myCallback = object : ServiceCallback() {
override fun onResult(theobject: Something) {
// your callback code here
}
override fun onFailure(ex: Throwable) {
// error handling
}
}
theService.enqueue(callback)
You can use suspendCoroutine
What it does is that it suspends execution until the continuation is satified by the callback. So you can write a KTX like the following:
suspend fun Service.getSomething(): Something = suspendCoroutine{ cont ->
val callback = object : ServiceCallback(){
override fun onSuccess(data: Something): Unit = cont.resume(data)
override fun onFailure(ex: Throwable): Unit = cont.resume(ex)
}
this.enqueue(callback)
}
I've create android application in which we used Firestore database.
I want to update firebase document within time.
Firestore is quick to post on server but when there is any connection issue then it taking time to save on Firestore
My task is if User tried to update withing few seconds eg 20 seconds if user can't post that data on server withing 20 seconds then later on that update should not be post on server
If user is online and have good connection then there is no problem but if there is poor connection then it will create problem to me.
There was feasibility in realtime database but can't see in firestore
onDisconnectRef.cancel
I don't know how to manage this cancelation state but I've tried this code.
It returning error that you can't do operation on running Firebase Instance
Please suggest me what I need to do.
val hashMap = HashMap<String, Any>()
hashMap["status"] = "ACCEPTED"
hashMap["time"] = time
app().firestoreDB
.collection("doc")
.document("id")
.update(hashMap)
.addOnSuccessListener {
isStoredOnServer = true
// my action
}
object : CountDownTimer(20000, 1000) {
override fun onTick(millisUntilFinished: Long) {
if(isStoredOnServer){
this.cancel()
}
}
override fun onFinish() {
if(!isStoredOnServer) {
FirebaseFirestore.getInstance().clearPersistence()
.addOnFailureListener { e -> Log.d("Firestore", "Error persistence writing document $e") }
.addOnSuccessListener { Log.d("Could enable persistence:") }
}
}
}.start()
Google is putting its Android API for accessing Google services (i.E. Google Drive) to rest and is replacing it with REST.
And while there is a 'migration guides', it fails to build a APK package ready for installation, because of 'Duplicate Class definition' or something.
For some reason it is incredibly hard to find some comprehensive information about how to access a Google Service using REST via Android (preferably using methods natively available to the OS).
After a lot of searching, puzzling, scratching my head, occasional swearing and a lot of learning about things I really didn't want to care about, I'd like to share a few pieces of code, that are actually working for me.
Disclaimer: I'm a rookie Android programmer (who really doesn't how to pick his battles), so if there are things in here, that have the real Android wizards shaking their heads, I hope you'll forgive me.
All code samples are written in Kotlin and Android Studio.
Worth noting: Only the 'application data folder' is queried in this little tutorial, you will need to adjust the requested scopes if you want to do something else.
Necessary preparations
Create a project and an OAuth key for your application as described here. Many of the information I gathered for authorization came from that place, so expect to find some similarities.
The Dashboard for your project may be found at https://console.developers.google.com/apis/dashboard
Add implementation "com.google.android.gms:play-services-auth:16.0.1" to your applications gradle file. This dependency will be used for authentication purposes.
Add 'internet' support to your applications manifest
<uses-permission android:name="android.permission.INTERNET"/>
Authenticating
The beginning of our journey is the authentication.
For this purpose, I used the GoogleSignIn Framework.
Create an activity (or use your main activity, your choice) and override the onActivityResult method there.
Add a block like this:
if (requestCode == RC_SIGN_IN) {
GoogleSignIn.getSignedInAccountFromIntent(data)
.addOnSuccessListener(::evaluateResponse)
.addOnFailureListener { e ->
Log.w(RecipeList.TAG, "signInResult:failed =" + e.toString())
evaluateResponse(null)
}
}
RC_REQUEST_CODE is an arbitrarily chosen ID value defined in the companion object as constant.
Once you want to perform authentication (i.E. by clicking of a button), you will need to start the activity we have just declared the callback for.
For this purpose, you need to prepare the authentication request first.
GoogleSignIn.getClient(this, GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestIdToken("YourClientIDGoesHere.apps.googleusercontent.com")
.requestScopes(Scope(Scopes.DRIVE_APPFOLDER))
.build())
This request gives you a client object you can start using straight away by calling.
startActivityForResult(client.signInIntent, RC_SIGN_IN)
This call will cause the authorization screen to pop up (if necessary), allow the user to select an account and then close itself again, passing the data to onActivityResult
To fetch the previously signed in user (without starting a new activity), you can also use the GoogleSignIn.getLastSignedInAccount(this); method in the background.
On failure either of these methods return null, so be ready to deal with that.
Now that we have an authenticated user, what do we do with it?
We ask for an auth token.
Right now, we only have an idToken in our account object, which is absolutely useless for what we want to do, because it doesn't allow us to call the API.
But Google comes to the rescue once more and supplies us with the GoogleAuthUtil.getToken(this, account.account, "oauth2:https://www.googleapis.com/auth/drive.appdata") call.
This call will forward the account information and return a String if all goes right: The auth token we need.
To be noted: This method performs a network request, meaning that it will throw up in your face, if you attempt to execute it in your UI thread.
I created a helper class which mimics the behavior (and API) of Googles 'Task' object, which takes care of the nitty gritty of calling a method on a thread and notifying the calling thread that it is done.
Save the auth token somewhere you can find it again, authorization is (finally) done with.
Querying the API
This part is far more straightforward than the previous one and goes hand in hand with the Google Drive REST API
All network requests need to be executed on a 'non-UI' thread, which is why I wrapped them up in my helper class to notify me once there is data to display.
private fun performNet(url: String, method: String, onSuccess: (JSONObject) -> Unit)
{
ThreadedTask<String>()
.addOnSuccess { onSuccess(JSONObject(it)) }
.addOnFailure { Log.w("DriveSync", "Sync failure $it") }
.execute(executor) {
val url = URL(url)
with (url.openConnection() as HttpURLConnection)
{
requestMethod = method
useCaches = false
doInput = true
doOutput = false
setRequestProperty("Authorization", "Bearer $authToken")
processNetResponse(responseCode, this)
}
}
}
private fun processNetResponse(responseCode: Int, connection: HttpURLConnection) : String
{
var responseData = "No Data"
val requestOK = (responseCode == HttpURLConnection.HTTP_OK)
BufferedReader(InputStreamReader(if (requestOK) connection.inputStream else connection.errorStream))
.use {
val response = StringBuffer()
var inputLine = it.readLine()
while (inputLine != null) {
response.append(inputLine)
inputLine = it.readLine()
}
responseData = response.toString()
}
if (!requestOK)
throw Exception("Bad request: $responseCode ($responseData)")
return responseData
}
This block of code is a rather generic helper function I put together from various sources and essentially just takes the URL to query, the method to perform (GET, POST, PATCH, DELETE) and constructs a HTTP request from it.
The auth token we got earlier during the authorization is passed as a header to the request to authenticate and identify ourselves as 'the user' to Google.
Google will, if everything is OK, reply with HTTP_OK (200) and onSuccess will be called, which will translate the JSON reply to a JSONObject, which will then be passed to the evaluation function we registered earlier.
Fetching the list of files
performNet("https://www.googleapis.com/drive/v3/files?spaces=appDataFolder", "GET")
The spaces parameter serves to tell Google, that we don't want to see the root folder but the application data folder. Without this parameter, the request would fail, because we only requested access to the appDataFolder.
The response should contain a JSONArray under the files key, which you then can parse and draw whatever information you want.
The ThreadTask class
This helper class encapsulates the steps necessary to perform an operation on a different context and perform a callback on the instantiating thread upon completion.
I am not claiming that this is THE way to this, it's just my 'Simply doesn't know any better'-way.
import android.os.Handler
import android.os.Looper
import android.os.Message
import java.lang.Exception
import java.util.concurrent.Executor
class ThreadedTask<T> {
private val onSuccess = mutableListOf<(T) -> Unit>()
private val onFailure = mutableListOf<(String) -> Unit>()
private val onComplete = mutableListOf<() -> Unit>()
fun addOnSuccess(handler: (T) -> Unit) : ThreadedTask<T> { onSuccess.add(handler); return this; }
fun addOnFailure(handler: (String) -> Unit) : ThreadedTask<T> { onFailure.add(handler); return this; }
fun addOnComplete(handler: () -> Unit) : ThreadedTask<T> { onComplete.add(handler);return this; }
/**
* Performs the passed code in a threaded context and executes Success/Failure/Complete handler respectively on the calling thread.
* If any (uncaught) exception is triggered, the task is considered 'failed'.
* Call this method last in the chain to avoid race conditions while adding the handlers.
*
*/
fun execute(executor: Executor, code: () -> T)
{
val handler = object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
super.handleMessage(msg)
publishResult(msg.what, msg.obj)
}
}
executor.execute {
try {
handler.obtainMessage(TASK_SUCCESS, code()).sendToTarget()
} catch (exception: Exception) {
handler.obtainMessage(TASK_FAILED, exception.toString()).sendToTarget()
}
}
}
private fun publishResult(returnCode: Int, returnValue: Any)
{
if (returnCode == TASK_FAILED)
onFailure.forEach { it(returnValue as String) }
else
onSuccess.forEach { it(returnValue as T) }
onComplete.forEach { it() }
// Removes all handlers, cleaning up potential retain cycles.
onFailure.clear()
onSuccess.clear()
onComplete.clear()
}
companion object {
private const val TASK_SUCCESS = 0
private const val TASK_FAILED = 1
}
}
The order of execution is important in this case.
You first need to add the callbacks to the class object and at the end you need to call execute and supply it with the executor you want to run the thread with and of course the code you want to execute.
It is not everything you can do with Google Drive, but it's a start and I hope this little compilation will save someone else some grief in the future.
I have an App with Firestore. I have a lot of Repositories. They work from Firestore. When I call 2 method in same time then I got an error.
class CommentRepository : CommentRepositoryInterface {
val firebaseFirestore = FirebaseFirestore.getInstance()
companion object {
const val COLLECTION_NAME = "post_comments"
const val COMMENT_POST_ID_KEY = "postid"
}
override fun getPostCommentsById(postId: String): Observable<CommentModel> {
return Observable.create { subscriber ->
firebaseFirestore.collection(COLLECTION_NAME)
.whereEqualTo(COMMENT_POST_ID_KEY, postId)
.get()
.addOnCompleteListener { task ->
if (task.isSuccessful) {
for (document in task.result) {
if (document.exists()) {
val documentModel = document.toObject(CommentModel::class.java)
subscriber.onNext(documentModel)
}
}
subscriber.onComplete()
} else {
subscriber.onError(task.exception!!) // TODO
}
}
}
}
}
The another one is almost same like that, but that one is using another collection.
So when I called these functions, then I got the next error:
Internal error in Firestore (0.6.6-dev).
Caused by: java.lang.RuntimeException: Failed to gain exclusive lock to the Firestore client's offline persistence. This generally means you are using Firestore from multiple processes in your app. Keep in mind that multi-process Android apps execute the code in your Application class in all processes, so you may need to avoid initializing Firestore in your Application class. If you are intentionally using Firestore from multiple processes, you can only enable offline persistence (i.e. call setPersistenceEnabled(true)) in one of them.
In the MyApplication class I tried to set the Singleton's of firestore settings.
val settings = FirebaseFirestoreSettings.Builder()
.setPersistenceEnabled(true)
.build()
FirebaseFirestore.getInstance().firestoreSettings = settings
I found it in Firestore's Doc:
For Android and iOS, offline persistence is enabled by default.
Anyone have idea to solve this problem?
I've cleared the App's Caching and the problem solved.
Do it or just remove from the phone! :)