1

I'm using Firestore in my Android app. I've created a class User, which represents a document under the users collection in my Firestore. This is the class:

public class User
{
    private String documentID;
    private String email;
    private String firstName;
    private String lastName;
    private String mobilePhone;

    private static final String USERS_COLLECTION_NAME = "users";

    private static class FieldNames
    {
        private static final String EMAIL = "email";
        private static final String FIRST_NAME = "firstName";
        private static final String LAST_NAME = "lastName";
        private static final String MOBILE_PHONE = "mobilePhone";
    }

    private User(String documentID)
    {
        this.documentID = documentID;
    }

    private static void checkRequiredStringField(DocumentSnapshot documentSnapshot, String fieldName) throws IllegalArgumentException
    {
        checkArgument(documentSnapshot.contains(fieldName), "The given documentSnapshot does not contain the required field '%s'.", fieldName);
    }

    private void attachUpdateEventListeners()
    {
        FirebaseFirestore.getInstance()
            .collection(USERS_COLLECTION_NAME)
            .document(documentID)
            .addSnapshotListener((documentSnapshot, exception) -> {
                if (exception == null)
                {
                    email = documentSnapshot.getString(FieldNames.EMAIL);
                    firstName = documentSnapshot.getString(FieldNames.FIRST_NAME);
                    lastName = documentSnapshot.getString(FieldNames.LAST_NAME);
                    mobilePhone = documentSnapshot.getString(FieldNames.MOBILE_PHONE);
                }
                else
                {
                    Crashlytics.log(Log.ERROR, createLogTagForClass(getClass()), "Exception occurred while listening for changes on document with ID " + documentID + ". Exception message: " + exception.getMessage());
                    Crashlytics.logException(exception);
                }
            });
    }

    @NonNull
    public static Task<DocumentSnapshot> getDocumentSnapshot(@NonNull FirebaseUser firebaseUser)
    {
        checkNotNull(firebaseUser);
        return FirebaseFirestore.getInstance().collection(USERS_COLLECTION_NAME)
                .document(firebaseUser.getUid())
                .get();
    }

    public static User fromDocumentSnapshot(@NonNull DocumentSnapshot documentSnapshot) throws IllegalArgumentException
    {
        checkNotNull(documentSnapshot);
        checkRequiredStringField(documentSnapshot, FieldNames.EMAIL);
        checkRequiredStringField(documentSnapshot, FieldNames.FIRST_NAME);
        checkRequiredStringField(documentSnapshot, FieldNames.LAST_NAME);
        checkRequiredStringField(documentSnapshot, FieldNames.MOBILE_PHONE);

        User user = new User(documentSnapshot.getId());

        user.email = documentSnapshot.getString(FieldNames.EMAIL);
        user.firstName = documentSnapshot.getString(FieldNames.FIRST_NAME);
        user.lastName = documentSnapshot.getString(FieldNames.LAST_NAME);
        user.mobilePhone = documentSnapshot.getString(FieldNames.MOBILE_PHONE);
        user.attachUpdateEventListeners();

        return user;
    }

    public String getEmail()
    {
        return email;
    }

    public String getFirstName()
    {
        return firstName;
    }

    public String getLastName()
    {
        return lastName;
    }

    public String getMobilePhone()
    {
        return mobilePhone;
    }
}

The idea of this class is to load it from Firestore (with the methods getDocumentSnapshot and fromDocumentSnapshot), then this class checks itself for updates (with the method attachUpdateEventListeners). The problem is that client code can call a getter method while the class is updating (due to an update fired by Firestore), so the client may get an out-dated value of the field.

I'm looking for a way to code something like this:

public String myGetter()
{
    if (isUpdateRunning())
    {
        waitForUpdate();
    }

    return myField;
}

but I can't figure out how to implement the two methods isUpdateRunning and waitForUpdate.

The only solution I've found is to put a flag isUpdateRunning in my User class, which will be set to true when the snapshotListener starts and to false when it finishes. Then, in my getters, if this flag's value is true I will wait a fixed amount of time (with a Thread.sleep) before returning the value. Though, I don't think this is the best solution (also because with Thread.sleep I would have to handle the InterruptedException).

private boolean isUpdateRunning;

private User(String documentID)
{
    this.documentID = documentID;
    isUpdateRunning = false;
}

private void attachUpdateEventListeners()
{
    FirebaseFirestore.getInstance()
        .collection(USERS_COLLECTION_NAME)
        .document(documentID)
        .addSnapshotListener((documentSnapshot, exception) -> {
            if (exception == null)
            {
                isUpdateRunning = true;
                // omitted code
                // re-assigning fields...
                isUpdateRunning = false;
            }
            // omitted code
        });
}

public String myGetter()
{
    if (isUpdateRunning)
    {
        Thread.sleep(1000); // how to handle the InterruptedException?
    }

    return myField;
}
Alex Mamo
  • 130,605
  • 17
  • 163
  • 193
Clyky
  • 247
  • 4
  • 14
  • Dont use the firestore code in your POJO class. Write an extra class that handle updates for you – Matze G. Aug 13 '18 at 14:58
  • It's strongly advisable never to block or sleep your app for any reason, unless you really know what you're doing, and you're doing it off the main thread. Instead, it's better to use asynchronous programming techniques to respond to changes in the database as they occur. – Doug Stevenson Aug 13 '18 at 15:32

1 Answers1

2

You cannot do that using those booleans and trying to call sleep(1000) isn't also helping you in all situations and its also called a bad practice in this case. What is happening if the time that it takes to get the data from the database is greater than 1 second? Your app will indefinitely freeze, which is a terrible user experience. Depending on your connection speed it may take from a few hundred milliseconds to a few seconds before that data is available. So there are no guarantees about how long it will take.

How to wait until a SnapshotListener has finished?

Firebase APIs are asynchronous, meaning that onEvent() method returns immediately after it's invoked, and the callback from the Task it returns, will be called some time later.

To avoid blocking the main thread, a quick solve for this problem would be to use those results only inside the onEvent() method, otherwise, if you'll want to wait for the data, I recommend you see the last part of my anwser from this post in which I have explained how it can be done using a custom callback. You can also take a look at this video for a better understanding.

Alex Mamo
  • 130,605
  • 17
  • 163
  • 193
  • ok, clear, so it's not possible to have a pojo class which handles itself updates from firestore. I think I will follow your suggestion on using custom callbacks, thank you :) – Clyky Aug 14 '18 at 08:08