ThierryC
ThierryC

Reputation: 1794

Firebase How to increment counter while offline?

In my Firebase Android Application, Every time a user likes a Post, I need to create a link between this user and the post post and increment the "number of likes" of this Post (this number can be > 10000 or more).
According to the doc. Here: https://firebase.google.com/docs/database/android/read-and-write#save_data_as_transactions
I can use a transaction to increment this counter. But I want to enable the Offline Capacity.

Problem: in the Firebase documentation it's written that "Transactions are not persisted across app restarts".

So how can I manage this use case: "a User likes 20 posts while offline and then stop the application"?

Upvotes: 2

Views: 1116

Answers (2)

Geraldo Neto
Geraldo Neto

Reputation: 4040

An easy way to do this is by running the transaction using WorkManager:

The WorkManager API makes it easy to schedule deferrable, asynchronous tasks that are expected to run even if the app exits or device restarts.

So your transaction is guaranteed to run!

Sample code:

public class TransactionWorker extends Worker {

private static final String TAG = "TransactionWorker";

private boolean success = false;
private boolean started = false;

public TransactionWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
    super(context, workerParams);
}

@NonNull
@Override
public Result doWork() {

    //database path
    String path = getInputData().getString("path");
    //increment amount
    long increment = getInputData().getLong("increment",0);

    DatabaseReference ref;
    try{
        //if database reference does not exist yet
        // (e.g. app not closed or database not started yet)
        ref = FirebaseDatabase.getInstance().getReference(path);

    }catch (NullPointerException error){
        error.printStackTrace();
        return Result.retry();
    }

    //wait while the transaction did not succeed yet
    while(!success){

        //only sleep if the transaction already started
        if(started){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            continue;
        }

        //start transaction only if not started yet
        ref.runTransaction(new Transaction.Handler() {
            @NonNull
            @Override
            public Transaction.Result doTransaction(@NonNull MutableData mutableData) {
                long value = 0;

                try{
                    value = mutableData.getValue(Long.class);
                }catch (Exception e){
                    e.printStackTrace();
                }

                //increment value
                mutableData.setValue(value + increment);
                return Transaction.success(mutableData);
            }

            @Override
            public void onComplete(@Nullable DatabaseError databaseError, boolean b, @Nullable DataSnapshot dataSnapshot) {

                Log.d(TAG, "onComplete: " + b);
                success = b;

            }
        });
        started = true;
    }

    //work succeeded
    return Result.success();
}}

From your Activity:

public static void doIncrementTransaction(String path,final long increment){

    Constraints constraints = new Constraints.Builder()
            .setRequiredNetworkType(NetworkType.CONNECTED).build();

    Data transactData = new Data.Builder().putString("path",path)
            .putLong("increment",increment).build();

   OneTimeWorkRequest oneTimeWorkRequest = new OneTimeWorkRequest.
            Builder(TransactionWorker.class).setInputData(transactData)
            .setConstraints(constraints)
            .setBackoffCriteria(BackoffPolicy.LINEAR,3000, TimeUnit.MILLISECONDS)
            .build();

    WorkManager.getInstance(this).enqueue(oneTimeWorkRequest);
}

That's it.

Hope it helps!

Upvotes: 1

Frank van Puffelen
Frank van Puffelen

Reputation: 599766

Since a transaction requires access to the current value of a field to determine the new value of a field, there is no way to run transactions while you're not connected to the database server.

The Firebase clients don't persist transactions across app restarts for the same reasons: the concept of transactions doesn't work well when a user is not connected.

If you want to record user actions while they are not connected, you should literally store that in your database: the user actions.

So instead of trying to increase the likeCount with a transaction, you could keep a list of likedPosts for the user:

likedPosts
  uidOfTooFoo
    post1: true
    post3: true
  uidOfTooPuf
    post2: true
    post3: true

With this structure you don't need a transaction to increase a counter, because each user is essentially isolated from everyone else.

Alternative, you could keep a queue of like actions:

likesQueue
  -K234782387432
    uid: "uidOfPoofoo"
    post: post1
  -K234782387433
    uid: "uidOfPuf"
    post: post2
  -K234782387434
    uid: "uidOfPuf"
    post: post3
  -K234782387434
    uid: "uidOfPoofoo"
    post: post3

With this last structure, you'd then spin up a small back-end service that consumes this queue (by listening for child_added events or preferably by using firebase-queue) and then increases the shared counter.

Upvotes: 3

Related Questions