Puis-je faire une requête synchrone avec volley?

133

Imaginez que je suis dans un service qui a déjà un fil d'arrière-plan. Puis-je faire une demande en utilisant volley dans ce même thread, afin que les rappels se produisent de manière synchrone?

Il y a 2 raisons à cela: - Premièrement, je n'ai pas besoin d'un autre fil et ce serait un gaspillage de le créer. - Deuxièmement, si je suis dans un ServiceIntent, l'exécution du thread se terminera avant le rappel, et je n'aurai donc pas de réponse de Volley. Je sais que je peux créer mon propre service qui a un fil avec une boucle d'exécution que je peux contrôler, mais il serait souhaitable d'avoir cette fonctionnalité dans volley.

Je vous remercie!

LocoMike
la source
5
Assurez-vous de lire la réponse de @ Blundell ainsi que la réponse hautement votée (et très utile).
Jedidja

Réponses:

183

On dirait que c'est possible avec la RequestFutureclasse de Volley . Par exemple, pour créer une requête JSON HTTP GET synchrone, vous pouvez effectuer les opérations suivantes:

RequestFuture<JSONObject> future = RequestFuture.newFuture();
JsonObjectRequest request = new JsonObjectRequest(URL, new JSONObject(), future, future);
requestQueue.add(request);

try {
  JSONObject response = future.get(); // this will block
} catch (InterruptedException e) {
  // exception handling
} catch (ExecutionException e) {
  // exception handling
}
Mat
la source
5
@tasomaniac Mis à jour. Cela utilise le JsonObjectRequest(String url, JSONObject jsonRequest, Listener<JSONObject> listener, ErrorListener errorlistener)constructeur. RequestFuture<JSONObject>implémente à la fois les interfaces Listener<JSONObject>et ErrorListener, de sorte qu'il peut être utilisé comme les deux derniers paramètres.
Matt
21
il bloque pour toujours!
Mohammed Subhi Sheikh Quroush
9
Astuce: cela peut bloquer pour toujours si vous appelez future.get () AVANT d'ajouter la requête à la file d'attente des requêtes.
datayeah
3
il sera bloqué pour toujours parce que vous pouvez avoir une erreur de connexion lire Blundell Answer
Mina Gabriel
4
On devrait dire que vous ne devriez pas faire cela sur le fil principal. Ce n'était pas clair pour moi. Parce que si le thread principal est bloqué par le, future.get()l'application se bloquera ou expirera à coup sûr si elle est définie.
r00tandy
125

Remarque La réponse @Matthews est correcte MAIS si vous êtes sur un autre thread et que vous faites un appel de volée lorsque vous n'avez pas Internet, votre rappel d'erreur sera appelé sur le thread principal, mais le thread sur lequel vous êtes sera bloqué POUR TOUJOURS. (Par conséquent, si ce thread est un IntentService, vous ne pourrez jamais lui envoyer un autre message et votre service sera pratiquement mort).

Utilisez la version de get()qui a un délai d'expirationfuture.get(30, TimeUnit.SECONDS) et détectez l'erreur pour quitter votre thread.

Pour correspondre à la réponse de @Mathews:

        try {
            return future.get(30, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            // exception handling
        } catch (ExecutionException e) {
            // exception handling
        } catch (TimeoutException e) {
            // exception handling
        }

Ci-dessous, je l'ai enveloppé dans une méthode et utilise une requête différente:

   /**
     * Runs a blocking Volley request
     *
     * @param method        get/put/post etc
     * @param url           endpoint
     * @param errorListener handles errors
     * @return the input stream result or exception: NOTE returns null once the onErrorResponse listener has been called
     */
    public InputStream runInputStreamRequest(int method, String url, Response.ErrorListener errorListener) {
        RequestFuture<InputStream> future = RequestFuture.newFuture();
        InputStreamRequest request = new InputStreamRequest(method, url, future, errorListener);
        getQueue().add(request);
        try {
            return future.get(REQUEST_TIMEOUT, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            Log.e("Retrieve cards api call interrupted.", e);
            errorListener.onErrorResponse(new VolleyError(e));
        } catch (ExecutionException e) {
            Log.e("Retrieve cards api call failed.", e);
            errorListener.onErrorResponse(new VolleyError(e));
        } catch (TimeoutException e) {
            Log.e("Retrieve cards api call timed out.", e);
            errorListener.onErrorResponse(new VolleyError(e));
        }
        return null;
    }
Blundell
la source
C'est un point assez important! Je ne sais pas pourquoi cette réponse n'a pas reçu plus de votes positifs.
Jedidja
1
Il convient également de noter que si vous transmettez l'ExecutionException au même écouteur que vous avez passé dans la requête, vous traiterez l'exception deux fois. Cette exception se produit lorsqu'une exception s'est produite lors de la requête, que volley passera à errorListener pour vous.
Stimsoni
@Blundell Je ne comprends pas votre réponse. Si l'écouteur est exécuté sur le thread d'interface utilisateur, vous avez un thread d'arrière-plan en attente et le thread d'interface utilisateur qui appelle notifyAll () donc c'est ok. Un blocage peut se produire si la livraison est effectuée sur le même thread que vous êtes bloqué avec le futur get (). Votre réponse semble donc dénuée de sens.
greywolf82
1
@ greywolf82 IntentServiceest un exécuteur de pool de threads d'un seul thread, donc IntentService sera bloqué pour toujours car il se trouve dans une boucle
Blundell
@Blundell je ne comprends pas. Il siège jusqu'à ce qu'une notification soit appelée et elle sera appelée à partir du thread d'interface utilisateur. Jusqu'à ce que vous ayez deux threads différents, je ne peux pas voir l'impasse
greywolf82
9

Il est probablement recommandé d'utiliser les Futures, mais si pour une raison quelconque vous ne le souhaitez pas, au lieu de préparer votre propre chose de blocage synchronisé, vous devriez utiliser un fichier java.util.concurrent.CountDownLatch. Cela fonctionnerait donc comme ça ...

//I'm running this in an instrumentation test, in real life you'd ofc obtain the context differently...
final Context context = InstrumentationRegistry.getTargetContext();
final RequestQueue queue = Volley.newRequestQueue(context);
final CountDownLatch countDownLatch = new CountDownLatch(1);
final Object[] responseHolder = new Object[1];

final StringRequest stringRequest = new StringRequest(Request.Method.GET, "http://google.com", new Response.Listener<String>() {
    @Override
    public void onResponse(String response) {
        responseHolder[0] = response;
        countDownLatch.countDown();
    }
}, new Response.ErrorListener() {
    @Override
    public void onErrorResponse(VolleyError error) {
        responseHolder[0] = error;
        countDownLatch.countDown();
    }
});
queue.add(stringRequest);
try {
    countDownLatch.await();
} catch (InterruptedException e) {
    throw new RuntimeException(e);
}
if (responseHolder[0] instanceof VolleyError) {
    final VolleyError volleyError = (VolleyError) responseHolder[0];
    //TODO: Handle error...
} else {
    final String response = (String) responseHolder[0];
    //TODO: Handle response...
}

Étant donné que les gens semblaient essayer de faire cela et rencontraient des problèmes, j'ai décidé de fournir un échantillon fonctionnel "réel" de cette utilisation. Le voici https://github.com/timolehto/SynchronousVolleySample

Maintenant, même si la solution fonctionne, elle présente certaines limites. Plus important encore, vous ne pouvez pas l'appeler sur le thread principal de l'interface utilisateur. Volley exécute les requêtes en arrière-plan, mais par défaut, Volley utilise le principal Looperde l'application pour envoyer les réponses. Cela provoque un blocage car le thread d'interface utilisateur principal attend la réponse, mais Looperattend la onCreatefin avant de traiter la livraison. Si vous voulez vraiment faire cela, vous pouvez, au lieu des méthodes d'assistance statiques, instancier la vôtre en la RequestQueuepassant la vôtreExecutorDelivery lié à un en Handlerutilisant un Looperqui est lié à un thread différent du thread principal de l'interface utilisateur.

Timo
la source
Cette solution bloque mon thread pour toujours, a changé Thread.sleep au lieu de countDownLatch et le problème est résolu
Snersesyan
Si vous pouvez fournir un échantillon complet d'un code échouant de cette manière, nous pourrons peut-être comprendre quel est le problème. Je ne vois pas en quoi dormir en conjonction avec des verrous de compte à rebours a du sens.
Timo
Très bien @VinojVetha J'ai mis à jour un peu la réponse pour clarifier la situation et fourni un dépôt GitHub que vous pouvez facilement cloner et essayer le code en action. Si vous rencontrez d'autres problèmes, veuillez fournir une fourchette de l'exemple de dépôt démontrant votre problème comme référence.
Timo
C'est une excellente solution pour les requêtes synchrones jusqu'à présent.
bikram
2

Comme observation complémentaire aux réponses @Blundells et @Mathews, je ne suis pas sûr qu'un appel soit livré à autre chose que le fil principal de Volley.

La source

En regardant l' RequestQueueimplémentation, il semble que l' RequestQueueutilisation de a NetworkDispatcherpour exécuter la requête et de a ResponseDeliverypour fournir le résultat (le ResponseDeliveryest injecté dans le NetworkDispatcher). Le ResponseDeliveryest à son tour créé avec un Handlerspawn du thread principal (quelque part autour de la ligne 112 dans leRequestQueue implémentation).

Quelque part à propos de la ligne 135 dans l' NetworkDispatcherimplémentation, il semble que des résultats positifs soient également fournis par le biais ResponseDeliveryde toute erreur. Encore; a ResponseDeliverybasé sur unHandler spawn du thread principal.

Raisonnement

Pour le cas d'utilisation où une demande doit être faite à partir d'un IntentService il est juste de supposer que le thread du service devrait se bloquer jusqu'à ce que nous ayons une réponse de Volley (pour garantir une portée d'exécution vivante pour gérer le résultat).

Solutions suggérées

Une approche serait de remplacer la manière par défaut de RequestQueuecréer a , où un constructeur alternatif est utilisé à la place, en injectant un ResponseDeliveryqui apparaît à partir du thread actuel plutôt que du thread principal. Cependant, je n'ai pas étudié les implications de cela.

dbm
la source
1
L'implémentation d'une implémentation ResponseDelivery personnalisée est compliquée par le fait que la finish()méthode de la Requestclasse et la RequestQueueclasse sont des packages privés, mis à part l'utilisation d'un hack de réflexion, je ne suis pas sûr qu'il y ait un moyen de contourner cela. Ce que j'ai fini par faire pour empêcher que quoi que ce soit ne s'exécute sur le thread principal (UI), a été de configurer un thread Looper alternatif (en utilisant Looper.prepareLooper(); Looper.loop()) et de passer une ExecutorDeliveryinstance au RequestQueueconstructeur avec un gestionnaire pour ce looper. Vous avez les frais généraux d'un autre boucleur mais restez en dehors du fil de discussion principal
Stephen James Hand
1

J'utilise un verrou pour obtenir cet effet maintenant, je me demande si c'est correct à ma façon que quelqu'un veut commenter?

// as a field of the class where i wan't to do the synchronous `volley` call   
Object mLock = new Object();


// need to have the error and success listeners notifyin
final boolean[] finished = {false};
            Response.Listener<ArrayList<Integer>> responseListener = new Response.Listener<ArrayList<Integer>>() {
                @Override
                public void onResponse(ArrayList<Integer> response) {
                    synchronized (mLock) {
                        System.out.println();
                        finished[0] = true;
                        mLock.notify();

                    }


                }
            };

            Response.ErrorListener errorListener = new Response.ErrorListener() {
                @Override
                public void onErrorResponse(VolleyError error) {
                    synchronized (mLock) {
                        System.out.println();
                        finished[0] = true;
                        System.out.println();
                        mLock.notify();
                    }
                }
            };

// after adding the Request to the volley queue
synchronized (mLock) {
            try {
                while(!finished[0]) {
                    mLock.wait();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
forcer
la source
Je pense que vous implémentez essentiellement ce que Volley fournit déjà lorsque vous utilisez des "futurs".
spaaarky21
1
Je recommanderais d'avoir l' catch (InterruptedException e)intérieur de la boucle while. Sinon, le thread n'attendra pas s'il est interrompu pour une raison quelconque
jayeffkay
@jayeffkay J'attrape déjà l'exception si une ** InterruptedException ** se produit dans la boucle while que la capture gère.
forcer le
1

Je veux ajouter quelque chose à la réponse acceptée de Matthew. Tandis queRequestFuture puisse sembler faire un appel synchrone à partir du thread que vous l'avez créé, ce n'est pas le cas. Au lieu de cela, l'appel est exécuté sur un thread d'arrière-plan.

D'après ce que je comprends après avoir parcouru la bibliothèque, les requêtes dans le RequestQueuesont réparties dans sa start()méthode:

    public void start() {
        ....
        mCacheDispatcher = new CacheDispatcher(...);
        mCacheDispatcher.start();
        ....
           NetworkDispatcher networkDispatcher = new NetworkDispatcher(...);
           networkDispatcher.start();
        ....
    }

Maintenant, les deux classes CacheDispatcheret NetworkDispatcherétendent thread. Ainsi, un nouveau thread de travail est généré pour retirer la file d'attente des demandes et la réponse est renvoyée aux écouteurs de succès et d'erreur implémentés en interne parRequestFuture .

Bien que votre deuxième objectif soit atteint, mais votre premier objectif ne l'est pas, car un nouveau thread est toujours généré, quel que soit le thread à partir duquel vous exécutez RequestFuture .

En bref, une véritable requête synchrone n'est pas possible avec la bibliothèque Volley par défaut. Corrigez-moi si je me trompe.

Vignatus
la source
0

Vous pouvez faire une demande de synchronisation avec volley mais vous devez appeler la méthode dans un thread différent ou votre application en cours d'exécution bloquera, cela devrait être comme ceci:

public String syncCall(){

    String URL = "http://192.168.1.35:8092/rest";
    String response = new String();



    RequestQueue requestQueue = Volley.newRequestQueue(this.getContext());

    RequestFuture<JSONObject> future = RequestFuture.newFuture();
    JsonObjectRequest request = new JsonObjectRequest(Request.Method.GET, URL, new JSONObject(), future, future);
    requestQueue.add(request);

    try {
        response = future.get().toString();
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    } catch (JSONException e) {
        e.printStackTrace();
    }

    return response;


}

après cela, vous pouvez appeler la méthode dans le thread:

 Thread thread = new Thread(new Runnable() {
                                    @Override
                                    public void run() {

                                        String response = syncCall();

                                    }
                                });
                                thread.start();
Slimani Ibrahim
la source