¿Puedo hacer una solicitud sincrónica con volley?

133

Imagine que estoy en un servicio que ya tiene un hilo de fondo. ¿Puedo hacer una solicitud usando volley en ese mismo hilo, para que las devoluciones de llamada se realicen sincrónicamente?

Hay 2 razones para esto: - Primero, no necesito otro hilo y sería un desperdicio crearlo. - Segundo, si estoy en un ServiceIntent, la ejecución del subproceso finalizará antes de la devolución de llamada y, por lo tanto, no tendré respuesta de Volley. Sé que puedo crear mi propio servicio que tiene algún hilo con un runloop que puedo controlar, pero sería deseable tener esta funcionalidad en volea.

¡Gracias!

LocoMike
fuente
55
Asegúrese de leer la respuesta de @ Blundell, así como la respuesta altamente votada (y muy útil).
Jedidja

Respuestas:

183

Parece que es posible con la RequestFutureclase de Volley . Por ejemplo, para crear una solicitud JSON HTTP GET síncrona, puede hacer lo siguiente:

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
}
Mate
fuente
55
@tasomaniac Actualizado. Esto usa el JsonObjectRequest(String url, JSONObject jsonRequest, Listener<JSONObject> listener, ErrorListener errorlistener)constructor. RequestFuture<JSONObject>implementa las interfaces Listener<JSONObject>y ErrorListener, por lo que puede usarse como los dos últimos parámetros.
Matt
21
se está bloqueando para siempre!
Mohammed Subhi Sheikh Quroush
9
Sugerencia: puede estar bloqueando para siempre si llama a future.get () ANTES de agregar la solicitud a la cola de solicitudes.
datayeah
3
se bloqueará para siempre porque puede tener un error de conexión leer Blundell Respuesta
Mina Gabriel
44
Uno debería decir que no debes hacer esto en el hilo principal. Eso no estaba claro para mí. Porque si el subproceso principal está bloqueado por la future.get()aplicación, entonces la aplicación se detendrá o se quedará sin tiempo de espera si está configurada.
r00tandy
125

Tenga en cuenta que la respuesta de @Matthews es correcta PERO si está en otro subproceso y realiza una llamada de volea cuando no tiene Internet, su devolución de llamada de error se llamará en el subproceso principal, pero el subproceso en el que se encuentre se bloqueará PARA SIEMPRE. (Por lo tanto, si ese hilo es un IntentService, nunca podrá enviarle otro mensaje y su servicio estará básicamente muerto).

Utilice la versión de get()que tiene un tiempo de esperafuture.get(30, TimeUnit.SECONDS) y detecte el error para salir de su hilo.

Para coincidir con la respuesta de @Mathews:

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

A continuación lo envolví en un método y uso una solicitud diferente:

   /**
     * 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
fuente
Ese es un punto bastante importante! No estoy seguro de por qué esta respuesta no ha recibido más votos a favor.
Jedidja
1
También vale la pena señalar que si pasa la ExecutionException al mismo oyente que pasó a la solicitud, procesará la excepción dos veces. Esta excepción se produce cuando se ha producido una excepción durante la solicitud, que la volea pasará al errorListener por usted.
Stimsoni
@Blundell No entiendo tu respuesta. Si el escucha se ejecuta en el subproceso de la interfaz de usuario, tiene un subproceso en segundo plano en espera y el subproceso de la interfaz de usuario que llama a notifyAll (), por lo que está bien. Un punto muerto puede ocurrir si la entrega se realiza en el mismo hilo que está bloqueado con el futuro get (). Entonces su respuesta parece no tener ningún sentido.
greywolf82
1
@ greywolf82 IntentServicees un ejecutor de grupo de subprocesos de un solo subproceso, por lo tanto, IntentService se bloqueará antes de que se asiente en un bucle
Blundell
@Blundell no entiendo. Se sienta hasta que se llame una notificación y se llamará desde el hilo de la interfaz de usuario. Hasta que tenga dos hilos diferentes, no puedo ver el punto muerto
greywolf82
9

Probablemente se recomienda usar los Futuros, pero si por alguna razón no desea hacerlo, en lugar de cocinar su propio bloqueo sincronizado, debe usar a java.util.concurrent.CountDownLatch. Entonces eso funcionaría así ...

//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...
}

Como la gente parecía tratar de hacer esto y se encontró con algunos problemas, decidí que en realidad proporcionaría una muestra de trabajo de la "vida real" de esto en uso. Aquí está https://github.com/timolehto/SynchronousVolleySample

Ahora, aunque la solución funciona, tiene algunas limitaciones. Lo más importante, no puede llamarlo en el hilo principal de la interfaz de usuario. Volley ejecuta las solicitudes en segundo plano, pero de forma predeterminada, Volley utiliza Looperla aplicación principal para enviar las respuestas. Esto provoca un punto muerto ya que el subproceso principal de la interfaz de usuario está esperando la respuesta, pero Looperestá esperando onCreateque termine antes de procesar la entrega. Si realmente desea hacer esto, podría, en lugar de los métodos auxiliares estáticos, crear una instancia de su propia RequestQueuetransmisión y pasarla ExecutorDeliverya un Handleruso Looperque esté vinculado a un hilo diferente del hilo principal de la interfaz de usuario.

Timo
fuente
Esta solución bloquea mi hilo para siempre, cambió Thread.sleep en lugar de countDownLatch y resolvió el problema
snersesyan
Si puede proporcionar una muestra completa de un código que falla de esta manera, tal vez podamos averiguar cuál es el problema. No veo cómo tiene sentido dormir junto con los pestillos de cuenta regresiva.
Timo
Muy bien @VinojVetha He actualizado la respuesta un poco para aclarar la situación y proporcioné un repositorio de GitHub que puedes clonar fácilmente e intentar el código en acción. Si tiene más problemas, proporcione un tenedor del repositorio de muestra que demuestre su problema como referencia.
Timo
Esta es una gran solución para solicitudes síncronas hasta ahora.
bikram
2

Como observación complementaria a ambos @Blundells y @Mathews respuestas, no estoy seguro de cualquier llamada se entrega a cualquier cosa , pero el hilo principal mediante voleo.

La fuente

Al echar un vistazo a la RequestQueueimplementación , parece que RequestQueueestá utilizando a NetworkDispatcherpara ejecutar la solicitud y a ResponseDeliverypara entregar el resultado ( ResponseDeliveryse inyecta en el NetworkDispatcher). A ResponseDeliverysu vez, se crea con un Handlerengendro del hilo principal (en algún lugar alrededor de la línea 112 en la RequestQueueimplementación).

En algún lugar sobre la línea 135 en la NetworkDispatcherimplementación , parece que también se entregan resultados exitosos a través de lo mismo ResponseDeliveryque cualquier error. De nuevo; una ResponseDeliverybasada en una Handlerfreza desde el hilo principal.

Razón fundamental

Para el caso de uso en el que se debe realizar una solicitud, es IntentServicejusto asumir que el hilo del servicio debe bloquearse hasta que tengamos una respuesta de Volley (para garantizar un alcance de tiempo de ejecución vivo para manejar el resultado).

Las soluciones sugeridas

Un enfoque sería anular la forma predeterminada en que RequestQueuese crea a , donde se utiliza un constructor alternativo, inyectando uno ResponseDeliveryque se genera a partir del hilo actual en lugar del hilo principal. Sin embargo, no he investigado las implicaciones de esto.

dbm
fuente
1
Implementar una implementación personalizada de ResponseDelivery es complicado por el hecho de que el finish()método en la Requestclase y la RequestQueueclase son paquetes privados, además de usar un truco de reflexión, no estoy seguro de que haya una forma de evitarlo. Lo que terminé haciendo para evitar que algo se ejecute en el hilo principal (UI) fue configurar un hilo Looper alternativo (usando Looper.prepareLooper(); Looper.loop()) y pasar una ExecutorDeliveryinstancia al RequestQueueconstructor con un controlador para ese looper. Tienes los gastos generales de otro looper pero mantente alejado del hilo principal
Stephen James Hand
1

Utilizo un candado para lograr ese efecto. Ahora me pregunto si es correcto para mí ¿alguien quiere comentar?

// 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();
            }
        }
fuerza de voluntad
fuente
Creo que esencialmente está implementando lo que Volley ya proporciona cuando usa "futuros".
spaaarky21
1
Recomendaría tener el catch (InterruptedException e)interior del bucle while. De lo contrario se producirá un error de rosca que esperar si se interrumpe por alguna razón
jayeffkay
@jayeffkay Ya estoy captando la excepción si se produce una ** InterruptedException ** en el ciclo while que la captura lo maneja.
forcewill
1

Quiero agregar algo a la respuesta aceptada de Matthew. Si bien RequestFuturepuede parecer que realiza una llamada sincrónica desde el subproceso que lo creó, no lo hace. En cambio, la llamada se ejecuta en un hilo de fondo.

Por lo que entiendo después de pasar por la biblioteca, las solicitudes en el RequestQueuese envían en su start()método:

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

Ahora ambos CacheDispatchery NetworkDispatcherclases extienden hilo. De manera efectiva, se genera un nuevo subproceso de trabajo para eliminar la cola de solicitudes y la respuesta se devuelve a los escuchas de éxito y error implementados internamente por RequestFuture.

Aunque se alcanza su segundo propósito, pero su primer propósito no lo es, ya que siempre se genera un nuevo hilo, sin importar desde qué hilo ejecute RequestFuture .

En resumen, la solicitud síncrona verdadera no es posible con la biblioteca Volley predeterminada. Corrígeme si estoy equivocado.

Vignatus
fuente
0

Puede hacer una solicitud de sincronización con volley pero debe llamar al método en un hilo diferente o su aplicación en ejecución se bloqueará, debería ser así:

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;


}

después de eso puedes llamar al método en hilo:

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

                                        String response = syncCall();

                                    }
                                });
                                thread.start();
Slimani Ibrahim
fuente