简体   繁体   中英

Android how to group async tasks together like in iOS

I have a function in iOS app that uses dispatch_group to group multiple rest request:

static func fetchCommentsAndTheirReplies(articleId: String, failure: ((NSError)->Void)?, success: (comments: [[String: AnyObject]], replies: [[[String: AnyObject]]], userIds: Set<String>)->Void) {
    var retComments = [[String: AnyObject]]()
    var retReplies = [[[String: AnyObject]]]()
    var retUserIds = Set<String>()

    let queue = dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0)
    Alamofire.request(.GET, API.baseUrl + API.article.listCreateComment, parameters: [API.article.articleId: articleId]).responseJSON {
        response in

        dispatch_async(queue) {

            guard let comments = response.result.value as? [[String: AnyObject]] else {
                failure?(Helper.error())
                return
            }
            print(comments)
            retComments = comments

            let group = dispatch_group_create()

            for (commentIndex, comment) in comments.enumerate() {
                guard let id = comment["_id"] as? String else {continue}

                let relevantUserIds = helperParseRelaventUserIdsFromEntity(comment)
                for userId in relevantUserIds {
                    retUserIds.insert(userId)
                }

                retReplies.append([[String: AnyObject]]())

                dispatch_group_enter(group)
                Alamofire.request(.GET, API.baseUrl + API.article.listCreateReply, parameters: [API.article.commentId: id]).responseJSON {
                    response in

                    dispatch_async(queue) {
                        if let replies = response.result.value as? [[String: AnyObject]] {
                            for (_, reply) in replies.enumerate() {

                                let relevantUserIds = helperParseRelaventUserIdsFromEntity(reply)
                                for userId in relevantUserIds {
                                    retUserIds.insert(userId)
                                }
                            }
                            retReplies[commentIndex] = replies
                        }
                        dispatch_group_leave(group)
                    }

                }
            }

            dispatch_group_wait(group, DISPATCH_TIME_FOREVER)
            success(comments: retComments, replies: retReplies, userIds: retUserIds)

        }

    }
}

As you can see from my code, I fetch all the comments under the same article , then fetch coresponding replies under each comment . After all requests are done, I invoke my success callback. This can be achieved using GCD's dispatch_group .

Now I am migrating the same functionality to android.

public static void fetchCommentsAndTheirReplies(Context context, String articleId, final StringBuffer outErrorMessage, final Runnable failure, final ArrayList<JSONObject> outComments, final ArrayList<ArrayList<JSONObject>> outReplies, final HashSet<String> outUserIds, final Runnable success) {
    final RequestQueue queue = Volley.newRequestQueue(context);
    HashMap<String, String> commentParams = new HashMap<>();
    commentParams.put(API.article.articleId, articleId);
    JsonArrayRequest commentRequest = new JsonArrayRequest(Request.Method.GET, API.baseUrl + API.article.listCreateComment, new JSONObject(commentParams), new Response.Listener<JSONArray>() {
        @Override
        public void onResponse(JSONArray response) {
            try {
                for (int i = 0; i < response.length(); i++) {
                    JSONObject comment = response.getJSONObject(i);
                    outComments.add(comment);

                    outUserIds.addAll(helperParseRelaventUserIdsFromEntity(comment));
                    outReplies.add(new ArrayList<JSONObject>());

                    //TODO: DISPATCH_GROUP?
                    String id = comment.getString("_id");
                    HashMap<String, String> replyParams = new HashMap<>();
                    replyParams.put(API.article.commentId, id);
                    final int finalI = i;
                    JsonArrayRequest replyRequest = new JsonArrayRequest(Request.Method.GET, API.baseUrl + API.article.listCreateReply, new JSONObject(replyParams), new Response.Listener<JSONArray>() {
                        @Override
                        public void onResponse(JSONArray response) {
                            try {
                                for (int j = 0; j < response.length(); j++) {
                                    JSONObject reply = response.getJSONObject(j);
                                    outUserIds.addAll(helperParseRelaventUserIdsFromEntity(reply));
                                    outReplies.get(finalI).add(reply);
                                }
                            } catch (JSONException ex) {}
                        }
                    }, new Response.ErrorListener() {
                        @Override
                        public void onErrorResponse(VolleyError error) {}
                    });
                    queue.add(replyRequest);
                }
                success.run();

            } catch (JSONException ex) {}
        }
    }, new Response.ErrorListener() {
        @Override
        public void onErrorResponse(VolleyError error) {
            outErrorMessage.append(error.getMessage());
            failure.run();
        }
    });
    queue.add(commentRequest);
}

Note that I am using success is executed right after I get all the comments , and before getting all the replies .

So how can I group them and delay the response?

I am working on the hairy implementation like

taskCount++;
if (taskCount == totalCount) {
    success.run();
} 

in reply block, but it seems very tedious.

You can simply do it with this class I made to mimic the iOS behavior. Call enter() and leave() the same way you did in iOS with dispatch_group_enter and dispatch_group_leave and call notify() just after the requests you want to group, just like dispatch_group_notify. It also uses runnable the same way iOS uses blocks :

public class DispatchGroup {

    private int count = 0;
    private Runnable runnable;

    public DispatchGroup()
    {
        super();
        count = 0;
    }

    public synchronized void enter(){
        count++;
    }

    public synchronized void leave(){
        count--;
        notifyGroup();
    }

    public void notify(Runnable r) {
        runnable = r;
        notifyGroup();
    }

    private void notifyGroup(){
        if (count <=0 && runnable!=null) {
             runnable.run();
        }
    }
}

Hope it helps ;)

Here is the Kotlin version of Damien Praca's answer. This will allow you to use Kotlin lambdas like this.

val dispatchGroup = DispatchGroup()
dispatchGroup.enter()
// Some long running task
dispatchGroup.leave()

dispatchGroup.notify {
// Some code to run after all dispatch groups complete
}

class DispatchGroup {
    private var count = 0
    private var runnable: (() -> Unit)? = null

    init {
        count = 0
    }

    @Synchronized
    fun enter() {
        count++
    }

    @Synchronized
    fun leave() {
        count--
        notifyGroup()
    }

    fun notify(r: () -> Unit) {
        runnable = r
        notifyGroup()
    }

    private fun notifyGroup() {
        if (count <= 0 && runnable != null) {
            runnable!!()
        }
    }
}

There is no direct analogue of dispatch_group in plain Java or Android. I can recommend a few rather sophisticated techniques to produce a really clean and elegant solution if you're ready to invest some extra time in it. It's not gonna be one or two lines of code, unfortunately.

  1. Use RxJava with parallelization . RxJava provides a clean way to dispatch multiple tasks, but it works sequentially by default. See this article to make it execute tasks concurrently.

  2. Although this is not exactly the intended usecase, you can try the ForkJoinPool to execute your group of tasks and recieve a single result afterwards.

Your "hairy" implementation isn't "hairy" at all imho.

public void onResponse(JSONArray response) {
                try {
                    final int[] taskFinished = {0};
                    final int taskTotal = response.length();
                    for (int i = 0; i < response.length(); i++) {
                        JSONObject comment = response.getJSONObject(i);
                        outComments.add(comment);

                        outUserIds.addAll(helperParseRelaventUserIdsFromEntity(comment));
                        outReplies.add(new ArrayList<JSONObject>());

                        //TODO: DISPATCH_GROUP?
                        String id = comment.getString("_id");
                        HashMap<String, String> replyParams = new HashMap<>();
                        replyParams.put(API.article.commentId, id);
                        final int finalI = i;
                        JsonArrayRequest replyRequest = new JsonArrayRequest(Request.Method.GET, API.baseUrl + API.article.listCreateReply, new JSONObject(replyParams), new Response.Listener<JSONArray>() {
                            @Override
                            public void onResponse(JSONArray response) {
                                taskFinished[0]++;
                                try {
                                    for (int j = 0; j < response.length(); j++) {
                                        JSONObject reply = response.getJSONObject(j);
                                        outUserIds.addAll(helperParseRelaventUserIdsFromEntity(reply));
                                        outReplies.get(finalI).add(reply);
                                    }
                                } catch (JSONException ex) {}
                                if (taskFinished[0] == taskTotal) {
                                    success.run();
                                }
                            }
                        }, new Response.ErrorListener() {
                            @Override
                            public void onErrorResponse(VolleyError error) {
                                taskFinished[0]++;
                                if (taskFinished[0] == taskTotal) {
                                    success.run();
                                }
                            }
                        });
                        queue.add(replyRequest);
                    }


                } catch (JSONException ex) {}
            }

You may use Thread s and Thread.join() with Handler s as an option.

quote from: https://docs.oracle.com/javase/tutorial/essential/concurrency/join.html

The join method allows one thread to wait for the completion of another. If t is a Thread object whose thread is currently executing,

t.join(); causes the current thread to pause execution until t's thread terminates. Overloads of join allow the programmer to specify a waiting period. However, as with sleep, join is dependent on the OS for timing, so you should not assume that join will wait exactly as long as you specify.

Like sleep, join responds to an interrupt by exiting with an InterruptedException.

EDIT : You should also check my event dispatcher gist. You may like it.

Try Priority Job Queue: https://github.com/yigit/android-priority-jobqueue

Priority Job Queue is an implementation of a Job Queue specifically written for Android to easily schedule jobs (tasks) that run in the background, improving UX and application stability.

(...)

You can group jobs to ensure their serial execution, if necessary. For example, assume you have a messaging client and your user sent a bunch of messages when their phone had no network coverage. When creating these SendMessageToNetwork jobs, you can group them by conversation ID. Through this approach, messages in the same conversation will send in the order they were enqueued, while messages between different conversations are still sent in parallel. This lets you effortlessly maximize network utilization and ensure data integrity.

I use java.util.concurrent. CountDownLatch to achieve the goal.
First of all I made a interface for each task.

interface GroupTask {
    void onProcessing(final CountDownLatch latch);
}

Then I create a class to handle grouping tasks.

interface MyDisptchGroupObserver {
    void onAllGroupTaskFinish();
}
class MyDisptchGroup {
    private static final int MSG_ALLTASKCOMPLETED = 300;
    private CountDownLatch latch;
    private MyDisptchGroupObserver observer;

    private MsgHandler msgHandler;
    private class MsgHandler extends Handler {
        MsgHandler(Looper looper) {
            super(looper);
        }
        @Override
        public void handleMessage(Message msg) {
            switch(msg.what) {
                case MSG_ALLTASKCOMPLETED:
                    observer.onAllGroupTaskFinish();
                    break;
                default:
                    break;
            }
        }
    }

    MyDisptchGroup(List<GroupTask> tasks, MyDisptchGroupObserver obj) {
        latch = new CountDownLatch(tasks.size());
        observer = obj;
        msgHandler = new MsgHandler(getActivity().getMainLooper())

        new Thread( new Runnable() {
            @Override
            public void run() {
                try {
                    latch.await();
                    Log.d(TAG, "========= All Tasks Completed =========");
                    msgHandler.sendEmptyMessage(MSG_ALLTASKCOMPLETED);
                } catch() {
                    e.printStackTrace();
                }
            }
        }).start();

        for( GroupTask task : tasks ) {
            task.onProcessing(latch);
        }
    }
}

Of course I have more than one task implementation as the following. The Task1

class Task1 implements GroupTask {
    @Override
    public void onProcessing(final CountDownLatch latch) {
        new Thread( new Runnable() {
            @Override
            public void run() {
                // Just implement my task1 stuff here


                // The end of the Task1 remember to countDown
                latch.countDown();
            }
        }).start();
    }
}

And Task2

class Task2 implements GroupTask {
    @Override
    public void onProcessing(final CountDownLatch latch) {
        new Thread( new Runnable() {
            @Override
            public void run() {
                // Just implement my task2 stuff here


                // The end of the Task2 remember to countDown
                latch.countDown();
            }
        }).start();
    }
}

Now everything are ready to fire.

ArrayList<GroupTask> allTasks = new ArrayList<GroupTask>();
allTasks.add(new Task1());
allTasks.add(new Task2());
new MyDisptchGroup(allTasks, this);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM