I'm having a little issue with FirebaseAuth.AuthStateListener while working with email verification on Firebase. I've verified my email by clicking the received verification link, and then I reloaded the current user by the lines of code below:
suspend fun reloadUserInfo() {
firebaseAuth.currentUser?.reload()?.await()
}
But AuthStateListener is not firing up even tho I reloaded the cached user. If I understood correctly AuthStateListener should trigger after reloading the current user. The reload() function's documentation says: Manually refreshes the data of the current user (for example, attached providers, display name, and so on). The isEmailVerified state changed the firebase user. Right?
val isEmailVerified: Flow<Boolean> = callbackFlow {
val authStateListener = AuthStateListener { auth ->
val isEmailVerified = auth.currentUser?.isEmailVerified == true
trySend(isEmailVerified)
}
firebaseAuth.addAuthStateListener(authStateListener)
awaitClose {
firebaseAuth.removeAuthStateListener(authStateListener)
}
}
This flow is not sending anything. But after restarting the application the callback gets fired. I don't want to restart the application to get the job done. It would not be a good user experience.
I did some research but nothing was found. If you take the time to help me, I appreciate it.
The email verification happens out of the band, so when you click the link in the email, there is nothing built-in that triggers Firebase Authentication clients to be updated. So there is no callback for that. Firebase only refreshes the ID token once per hour. This means that it may take up to an hour before the token is refreshed, a case in which the onIdTokenChanged() method fires. So onAuthStateChanged() nor onIdTokenChanged() fires when the link is clicked, which basically means that we have to check that in our application code, on demand. Since you're using Kotlin, the solution is quite simple:
return try {
auth.currentUser?.reload()?.await()
} catch (e: Exception) {
Log.e(TAG, "${e.message}")
}
However, do not attach a complete listener and call await() at the same time. It's one or the other. Do not combine them! Why? Because there is no guarantee that the listener will be called before or after await(). That means that there is a chance that the code in the complete listener won't be called until or after the suspend function returns. Besides that, one of the major reasons to use Kotlin Coroutines in the first place is to avoid using callbacks, which are not life-cycle aware.
If you want to see a concrete example, this resource will help. Here is the corresponding repo.
I'm doing some tests and I cannot understand the different behaviour when I use the GlobalScope.launch and viewModelScope CoroutineScope
I have the following code in a viewModel:
Log.d("init")
GlobalScope.launch((Dispatchers.Main)) {
repository
.storedItemsListener()
.onStart {
Log.d("onStart")
}
.onEach {
Log.d("onEach")
}
.onEmpty {
Log.d("onEmpty")
}
.onCompletion {
Log.d("onCompletion")
}
.launchIn(this)
}
Whenever the database is updated with items, storedItemsListener logs Storage Updated
At app launch, I perform a network request that updates storage with items. I then do a pull to refresh that performs a new request that also stores items on the database. With the above code, I have these logs:
// After app launch
init
onStart
Storage Updated //when the listener on the database is triggered
onEach
//After pull to refresh, although I know I store items on the database, no logs are produced. It seems that is the coroutine scope is dead and stops responding.
I then change the above code to use viewModel (.launchIn(viewModelScope)). I then obtain the logs that I expect.
// After app launch
init
onStart
Storage Updated //when the listener on the database is triggered
onEach
Storage Updated //when storage is updated with network request result
onEach
//After pull to refresh
Storage Updated //when storage is updated with network request result
onEach
My question is this. Shouldn't GlobalScope.launch be kept “alive” and notify me of all storage updates?
Please note that I want to keep this mechanism alive always and not only bound to a viewModel scope, and this is why I've chosen Global scope. The above description is a simplified version of what I need.
After deleting data from my Firestore Database, it takes my Android app some time to realize that the data was deleted, and I assume that it's happening due the auto data cache. My app has nothing to do with offline usage and I'd like to disable this feature...
I have added this in my custom Application Class:
import android.app.Application;
import com.google.firebase.firestore.FirebaseFirestore;
import com.google.firebase.firestore.FirebaseFirestoreSettings;
public class ApplicationClass extends Application {
#Override
public void onCreate() {
super.onCreate();
FirebaseFirestore db=FirebaseFirestore.getInstance();
FirebaseFirestoreSettings settings = new FirebaseFirestoreSettings.Builder()
.setPersistenceEnabled(false)
.build();
db.setFirestoreSettings(settings);
}
}
The problem occurs after turning off the internet connection and than turning it back on (while the app is still running, in the background or not)- the Firestore module seems to lose connection to the server, and it makes the opposite operation than the intended one - instead of stop taking data from the cache, it takes data from the cache only.
For example, debugging this code will always show that isFromCache is true and documentSnapshot is empty (even though that on the server side - it's not empty):
usersRef.document(loggedEmail).collection("challenges_received").get().addOnSuccessListener(new OnSuccessListener<QuerySnapshot>() {
#Override
public void onSuccess(QuerySnapshot documentSnapshots) {
boolean isFromCache=documentSnapshots.getMetadata().isFromCache();
if (!documentSnapshots.isEmpty()) {
}
}
});
Is this normal behavior?
Is there another way to disable the data cache in Cloud Firestore?
EDIT:
Adding: FirebaseFirestore.setLoggingEnabled(flase); (instead of the code above) in the custom Application Class gives the same result.
According to Cloud Firestore 16.0.0 SDK update, there is now a solution to this problem:
You are now able to choose if you would like to fetch your data from the server only, or from the cache only, like this (an example for server only):
DocumentReference documentReference= FirebaseFirestore.getInstance().document("example");
documentReference.get(Source.SERVER).addOnSuccessListener(new OnSuccessListener<DocumentSnapshot>() {
#Override
public void onSuccess(DocumentSnapshot documentSnapshot) {
//...
}
});
For cache only, just change the code above to Source.CACHE.
By default, both methods still attempt server and fall back to the cache.
I just ran a few tests in an Android application to see how this works. Because Firestore is currently still in beta release and the product might suffer changes any time, i cannot guarantee that this behaviour will still hold in the future.
db.collection("tests").document("fOpCiqmUjAzjnZimjd5c").get().addOnCompleteListener(new OnCompleteListener<DocumentSnapshot>() {
#Override
public void onComplete(#NonNull Task<DocumentSnapshot> task) {
DocumentSnapshot documentSnapshot = task.getResult();
System.out.println("isFromCache: " + documentSnapshot.getMetadata().isFromCache());
}
});
Regarding the code, is the same no matter if we're getting the data from the cache or you are connected to the servers.
When I'm online it prints:
isFromCache: false
When I'm offline, it prints:
isFromCache: true
So, for the moment, there is no way to stop the retrieval of the data from the cache while you are not connected to the server, as you cannot force the retrieval of the data from the cache while you're connected to the server.
If instead I use a listener:
db.collection("tests").document("fOpCiqmUjAzjnZimjd5c").addSnapshotListener(new DocumentListenOptions().includeMetadataChanges(), new EventListener<DocumentSnapshot>() {
#Override
public void onEvent(DocumentSnapshot documentSnapshot, FirebaseFirestoreException e) {
System.out.println("listener.isFromCache: " + documentSnapshot.getMetadata().isFromCache());
}
});
I get two prints when I'm online:
listener.isFromCache: true
listener.isFromCache: false
Firestore is desinged to retrieve data from the chache when the device is permanently offline or while your application temporarily loses its network connection and for the moment you cannot change this behaviour.
As a concusion, an API that does something like this, currently doesn't exist yet.
Edit: Unlike in Firebase, where to enable the offline persistence you need use this line of code:
FirebaseDatabase.getInstance().setPersistenceEnabled(true);
In Firestore, for Android and iOS, offline persistence is enabled by default.
Using the above line of code, means that you tell Firebase to create a local (internal) copy of your database so that your app can work even if it temporarily loses its network connection.
In Firestore we find the opposite, to disable persistence, we need to set the PersistenceEnabled option to false. This means that you tell Firestore not to create a local copy of your database on user device, which in term means that you'll not be able to query your database unless your are connected to Firebase servers. So without having a local copy of your database and if beeing disconected, an Exception will be thrown. That's why is a good practice to use the OnFailureListener.
Update (2018-06-13): As also #TalBarda mentioned in his answer this is now possible starting with the 16.0.0 SDK version update. So we can achieve this with the help of the DocumentReference.get(Source source) and Query.get(Source source) methods.
By default, get() attempts to provide up-to-date data when possible by waiting for data from the server, but it may return cached data or fail if you are offline and the server cannot be reached. This behavior can be altered via the Source parameter.
So we can now pass as an argument to the DocumentReference or to the Query the source so we can force the retrieval of data from the server only, chache only or attempt server and fall back to the cache.
So something like this is now possible:
FirebaseFirestore db = FirebaseFirestore.getInstance();
DocumentReference docIdRef = db.collection("tests").document("fOpCiqmUjAzjnZimjd5c");
docIdRef.get(Source.SERVER).addOnSuccessListener(new OnSuccessListener<DocumentSnapshot>() {
#Override
public void onSuccess(DocumentSnapshot documentSnapshot) {
//Get data from the documentSnapshot object
}
});
In this case, we force the data to be retrieved from the server only. If you want to force the data to be retrieved from the cache only, you should pass as an argument to the get() method, Source.CACHE. More informations here.
FirebaseFirestoreSettings settings = new FirebaseFirestoreSettings.Builder()
.setPersistenceEnabled(false)
.build();
dbEventHome.setFirestoreSettings(settings);
By setting this it is fetching from server always.
In Kotlin:
val db:FirebaseFirestore = Firebase.firestore
val settings = firestoreSettings {
isPersistenceEnabled = false
}
db.firestoreSettings = settings
// Enable Firestore logging
FirebaseFirestore.setLoggingEnabled(flase);
// Firestore
mFirestore = FirebaseFirestore.getInstance();
In general: the Firebase client tries to minimize the number of times it downloads data. But it also tries to minimize the amount of memory/disk space it uses.
The exact behavior depends on many things, such as whether the another listener has remained active on that location and whether you're using disk persistence. If you have two listeners for the same (or overlapping) data, updates will only be downloaded once. But if you remove the last listener for a location, the data for that location is removed from the (memory and/or disk) cache.
Without seeing a complete piece of code, it's hard to tell what will happen in your case.
Alternatively: you can check for yourself by enabling Firebase's logging [Firebase setLoggingEnabled:YES];
try this For FireBase DataBase
mDatabase.getReference().keepSynced(false);
FirebaseDatabase.getInstance().setPersistenceEnabled(false);
In Kotlin;
val settings = FirebaseFirestoreSettings.Builder()
with(settings){
isPersistenceEnabled = false
}
Firebase.firestore.firestoreSettings = settings.build()
TL;DR Looking for recommendations on robust offline support using rx-kotlin.
I've followed a nice guide on offline-support in Android apps.
It works like this:
Load data from the Internet, go to step 3 if error
Store the data in the local database
Load the data from the local database
Display the data in the UI
The code is:
Observable.mergeDelayError(
loadRemoteData()
.doOnNext { writeDataToLocalDatabase(it) }
.subscribeOn(Schedulers.io()),
loadDataFromLocalDatabase()
.subscribeOn(Schedulers.io())
)
Unfortunately, this approach relies on the database code always working. If the database operations for some reason fail, everything fails, even when the data is loaded successfully from the remote server.
Is there a way I can achieve the following using rx-kotlin/rx-java?:
Load data from the Internet, go to step 3 if error
Store the data in the local database
Load the data from the local database
(if steps 2 or 3 failed) Use the data from step 1
Display the data in the UI
I'd like to avoid loading the data from the Internet twice. I'm using room + retrofit, if that matters.
EDIT:
Thanks to #MinseongPark, I ended up with the code below.
The mergeDelayError reports an error only if both the remote and the local source fails, and if the writeDataToLocalDatabase method fails (throws an Exception), then that does not keep the remote data from being reported to the UI. The information about errors in writeDataToLocalDatabase is saved by reporting it remotely.
Now, the code is robust to one of the two sources failing, and to writing new entries to the database failing.
return Observable.mergeDelayError(
loadRemoteData().doOnNext {
try {
writeDataToLocalDatabase(it)
} catch (error: Throwable) {
Timber.d(error)
Crashlytics.logException(error)
}
},
loadDataFromLocalDatabase()
)
.subscribeOn(Schedulers.io())
Try this.
Observable.mergeDelayError(
loadRemoteData()
.doOnNext { runCatching { writeDataToLocalDatabase(it) } }
.subscribeOn(Schedulers.io()),
loadDataFromLocalDatabase()
.onErrorResumeNext(Observable.empty())
.subscribeOn(Schedulers.io())
)
In my Android application after successful login I'm saving session info in Room, then I'm retrieving user information from BE and saving it too.
Everything works fine. I can see saved information in database tables.
When user logs out from application all tables are being cleared with appDatabase.clearAllTables() method call.
The catch is that on subsequent login there's no information that is being inserted in DB but my Rx calls aren't throwing any errors.
I've tried to use logging and debug but everything looks like normal.
Logging shows following actions being performed: login -> get user info from BE -> save session -> save user.
When debugging I can see that user info is being handled in UserDao_Impl's insertUser() method.
In application I use RxJava2 version 2.2.2, Room persistence library version 1.1.1, dependencies are being provided with Dagger version 2.19.
Here's my code snippets:
LoginUserUseCase.java
public Completable execute(#NonNull LoginInfo loginInfo) {
Completable loginAndSaveSession = sessionRepository.loginUser(loginInfo)
.flatMapCompletable(
session -> sessionRepository.saveSession(
session));
Completable getAndSaveUserInfo = userRepository.getRemoteUserInfo()
.flatMapCompletable(
user -> userRepository.saveUser(
user));
return loginAndSaveSession.andThen(getAndSaveUserInfo);
}
UserRepository.java
public Completable saveUser(#NonNull User user) {
return Completable.fromAction(() -> userDao.insertUser(user));
}
UserDao.java
#Insert(onConflict = OnConflictStrategy.REPLACE)
void insertUser(User user);
LogoutUserUseCase.java
public Completable execute() {
return Completable.fromAction(() -> appDatabase.clearAllTables());
}
UPDATE (ISSUE RESOLVED): wasted a day on this bug. Finally come to understanding that some other library can affect application's work with Room. Turned out that Android Debug Database library that I've used to peek inside the database on device has a bug that breaks DB after you opened it once with its help.
Returning back to Stetho.