Replicating Swift completion handler on Android & Java
Asked Answered
F

3

6

After years, I'm trying to develop an Android app, using Firebase Firestore. I'm basically trying to replicate this Swift function:

func getCategories(onCompletion completionBlock: @escaping (_ categories: [Category]?, _ error: Error?) -> Void) {

  firestore.collection("cats").getDocuments { (snap, error) in
    guard let snap = snap else {
      completionBlock(nil, error ?? anUnknownError)
      return
    }

    var categories: [Category] = []
    for document in snap.documents {
        let cat = Category.init(data: document.data())
        categories.append(cat)
    }
    completionBlock(categories, nil)
  }
}

But I have no idea what is the equivalent of swift's blocks, even don't know if it exists.

I checked Firebase source codes. Query.get() returns Task<QuerySnapshot> so I tried to return a Task<List<Category>> without luck.

Any Help? Thank you.

EDIT: Android code added to clarify what I'm trying to do.

public class FirestoreService {

    private static volatile FirestoreService singleton = new FirestoreService();

    public static FirestoreService getInstance() {
        return singleton;
    }

    private FirebaseFirestore firestore() {
        // default firestore instance
        FirebaseFirestore db = FirebaseFirestore.getInstance();

        // default firestore settings
        FirebaseFirestoreSettings settings = db.getFirestoreSettings();

        // firestore settings builder
        FirebaseFirestoreSettings.Builder builder = new FirebaseFirestoreSettings.Builder(settings);

        // enable timstamps
        builder.setTimestampsInSnapshotsEnabled(true);

        // set new settings to db instance
        db.setFirestoreSettings(builder.build());


        // return db with new settings.
        return db;
    }



    public void getProductCategories(Handler? handler) {

       Task<QuerySnapshot> task = firestore().collection("coll").get();
       task.addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
        @Override
        public void onComplete(@NonNull Task<QuerySnapshot> task) {
            try {
                if (task.isSuccessful()) {
                    List<Category> cats = new ArrayList<>();
                    for (QueryDocumentSnapshot doc : task.getResult()) {
                        String id = doc.getId();
                        Map<String, Object> data = doc.getData();
                        Category cat = new Category(id, data);
                        cats.add(cat);
                    }
                    // now I need completion handler
                } else {
                    Log.w("ERROR", "Error getting categories", task.getException());
                }
            } catch (Exception e) {
                Log.e("ERROR", e.getMessage());
            }
        }
    });
    }

}



public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        FirestoreService.getInstance().getCategories().addCompletionListener(
            // handle List<Category> and respresent in UI
        );
    }
}
Froh answered 30/12, 2018 at 16:41 Comment(0)
R
4

Using AsyncTask

You can use an AsyncTask. It has 3 steps to it.

1. onPreExecute() - things you want to do before running doInBackground(). This happens in the UI main thread.

2. doInBackground()- the AsyncTask, will do operations in a background thread (the background thread is created by Android so you don't need to worry about it).

3.onPostExecute() - here you can receive any data from the doInBackground method. The postExecute method is executed again, in the UI main thread.

So you can do any I/O operations in doInBackground(), and return the value you received from the server or any other data source, and onPostExecute(), is the equivalent of a completion block in swift.

How to Declare

To use AsyncTask, you need to extend the Android AsyncTask.

So your own AsyncTask declaration will look like this:

private class MyAsyncTask extends AsyncTask<Void, Void, Void> { ... }

What are the 3 generic arguments you ask?

1. Params - the type of the parameters sent to the task upon execution.

2. Progress - the type of the progress units published during the background computation. (Almost always will be Void, unless you care about the actual progress of the operation. Notice this is Void with a capital letter, and not void as the return type).

3. Result - the type of the result of the background computation.

Full Example

private class LongOperation extends AsyncTask<String, Void, String> {

        @Override
        protected String doInBackground(String... params) {
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    Thread.interrupted();
                }
            }
            return "Executed";
        }

        @Override
        protected void onPostExecute(String result) {
            TextView txt = findViewById(R.id.output);
            txt.setText(result); 
        }
    }

In the example, I create a fake, long operation, that you can not run on the UI main thread (because it is a blocking operation).

When the operation is finished, it returns a String, and that same String is received in the onPostExecute() method (and remember, onPostExecute() runs on the UI main thread again). So you can change your UI with the String value you received from the long,blocking operation.

If you want the documentation, here it is:

https://developer.android.com/reference/android/os/AsyncTask

Using Observer Pattern

You can also use the observer pattern in your situation.

Create an interface, that has a method onSuccess(). Have an object implement that interface, and whenever you need it, you can call the onSuccess() method.

Example:

public Interface SuccessInterface{
   void onSuccess()
}

public class SuccessHandler implements SuccessInterface{
   public void onSuccess(){
         //success code goes here
   }

}

then in your code, have a SucessHandler instantiated, and call onSuccess() when you need to.

Rusell answered 30/12, 2018 at 16:53 Comment(5)
Would you share a code representation of what you say? As I said, I'm not developing Android and Java for 6 years, and I was not an expert of them too. Now I'm really frustrated and confused.Pertinacity
@ArdaOğulÜçpınar I hope this answers your question :) If something is still unclear please comment here and I will reply.Rusell
Uhm. It should do the job, but I do not know if it sits to my use case. I'm actually trying to create a service for Firestore things; to keep data fetching and data parsing out of the controller. And I'm not sure how can I implement this on what I'm trying to do. I'm editing my question, to clarify what I'm doing.Pertinacity
@ArdaOğulÜçpınar I looked at your edit, why not just use the observer design pattern? (a delegate in swift terms). I don't understand why you need a completion handler there, is it a blocking operation or something?Rusell
Actually, CH is what I use on Swift. Since I do not know much about Java's capabilities, my ideas are stuck with the completion handler. But if there is a delegate-like pattern, of course I could use it. Thanks @daniel-bPertinacity
F
10

Thank you very much four your help and lead @Daniel-b.

I've solved my issue now.

First I created an Interface for handling results; as you suggested.

public interface ResultHandler<T> {
    void onSuccess(T data);
    void onFailure(Exception e);
}

Then in the service class, I added ResultHandler to the function's input parameters :

public void getUserInfo(String id, ResultHandler<UserInfo> handler) {
    firestore().collection("userInfo").document(id).get().addOnCompleteListener(snap -> {
        if (snap.isSuccessful()) {
           try {
               // failable constructor. use try-catch
               UserInfo info = new UserInfo(snap.getResult().getId(), snap.getResult().getData());
               handler.onSuccess(info);
           } catch (Exception e) {
                handler.onFailure(e);
           }
        } else {
            handler.onFailure(snap.getException())
        }
    });
}

And called service in Activity

FirestoreService.getInstance().getUserInfo("ZsrAdsG5HVYLTZDBeZtkGDlIBW42", new ResultHandler<UserInfo>() {
    @Override
    public void onSuccess(UserInfo data) {
        Log.i("UserInfo", data.id);                    
    }

    @Override
    public void onFailure(Exception e) {
        // getting data failed for some reason
    }
});
Froh answered 31/12, 2018 at 16:3 Comment(3)
A big +1 on my part because this also works with an AsyncTask (I know, I know, they're deprecated in newer versions): Make the handler final, then call handler.someMethod() in onPostExecute. This seems to be a good and easy way of replicating a DispatchQueue.main.async {completion(something)} inside a DispatchQueue.global(qos: .background).async {//Some code} and it doesn't require huge amounts of code compared to the original! :)Abercromby
@Arda does this work with firebase realtime database or only firestore? I was under the impression that you could not do completion handlers with realtime DB in android as is in Swift?!Erle
@Erle Well, you should make it work following the same pattern. I am not using Firebase for a long time now, so I cannot share examplesPertinacity
R
4

Using AsyncTask

You can use an AsyncTask. It has 3 steps to it.

1. onPreExecute() - things you want to do before running doInBackground(). This happens in the UI main thread.

2. doInBackground()- the AsyncTask, will do operations in a background thread (the background thread is created by Android so you don't need to worry about it).

3.onPostExecute() - here you can receive any data from the doInBackground method. The postExecute method is executed again, in the UI main thread.

So you can do any I/O operations in doInBackground(), and return the value you received from the server or any other data source, and onPostExecute(), is the equivalent of a completion block in swift.

How to Declare

To use AsyncTask, you need to extend the Android AsyncTask.

So your own AsyncTask declaration will look like this:

private class MyAsyncTask extends AsyncTask<Void, Void, Void> { ... }

What are the 3 generic arguments you ask?

1. Params - the type of the parameters sent to the task upon execution.

2. Progress - the type of the progress units published during the background computation. (Almost always will be Void, unless you care about the actual progress of the operation. Notice this is Void with a capital letter, and not void as the return type).

3. Result - the type of the result of the background computation.

Full Example

private class LongOperation extends AsyncTask<String, Void, String> {

        @Override
        protected String doInBackground(String... params) {
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    Thread.interrupted();
                }
            }
            return "Executed";
        }

        @Override
        protected void onPostExecute(String result) {
            TextView txt = findViewById(R.id.output);
            txt.setText(result); 
        }
    }

In the example, I create a fake, long operation, that you can not run on the UI main thread (because it is a blocking operation).

When the operation is finished, it returns a String, and that same String is received in the onPostExecute() method (and remember, onPostExecute() runs on the UI main thread again). So you can change your UI with the String value you received from the long,blocking operation.

If you want the documentation, here it is:

https://developer.android.com/reference/android/os/AsyncTask

Using Observer Pattern

You can also use the observer pattern in your situation.

Create an interface, that has a method onSuccess(). Have an object implement that interface, and whenever you need it, you can call the onSuccess() method.

Example:

public Interface SuccessInterface{
   void onSuccess()
}

public class SuccessHandler implements SuccessInterface{
   public void onSuccess(){
         //success code goes here
   }

}

then in your code, have a SucessHandler instantiated, and call onSuccess() when you need to.

Rusell answered 30/12, 2018 at 16:53 Comment(5)
Would you share a code representation of what you say? As I said, I'm not developing Android and Java for 6 years, and I was not an expert of them too. Now I'm really frustrated and confused.Pertinacity
@ArdaOğulÜçpınar I hope this answers your question :) If something is still unclear please comment here and I will reply.Rusell
Uhm. It should do the job, but I do not know if it sits to my use case. I'm actually trying to create a service for Firestore things; to keep data fetching and data parsing out of the controller. And I'm not sure how can I implement this on what I'm trying to do. I'm editing my question, to clarify what I'm doing.Pertinacity
@ArdaOğulÜçpınar I looked at your edit, why not just use the observer design pattern? (a delegate in swift terms). I don't understand why you need a completion handler there, is it a blocking operation or something?Rusell
Actually, CH is what I use on Swift. Since I do not know much about Java's capabilities, my ideas are stuck with the completion handler. But if there is a delegate-like pattern, of course I could use it. Thanks @daniel-bPertinacity
A
1

For API 26, CompletionHandler is available. Check https://developer.android.com/reference/java/nio/channels/CompletionHandler

Auspicious answered 7/9, 2021 at 5:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.