Posso fazer uma solicitação síncrona com vôlei?

133

Imagine que eu esteja em um Serviço que já tenha um encadeamento em segundo plano. Posso fazer uma solicitação usando vôlei no mesmo thread, para que os retornos de chamada ocorram de forma síncrona?

Existem duas razões para isso: - Primeiro, não preciso de outro thread e seria um desperdício criá-lo. - Segundo, se eu estiver em um ServiceIntent, a execução do encadeamento será concluída antes do retorno de chamada e, portanto, não terei resposta da Volley. Eu sei que posso criar meu próprio serviço que tem algum segmento com um runloop que posso controlar, mas seria desejável ter essa funcionalidade no voleio.

Obrigado!

LocoMike
fonte
5
Leia a resposta de @ Blundell, bem como a resposta altamente votada (e muito útil).
Jedidja #

Respostas:

183

Parece que é possível com a RequestFutureaula de Volley . Por exemplo, para criar uma solicitação JSON HTTP GET síncrona, você pode fazer o seguinte:

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
}
Matt
fonte
5
@tasomaniac Atualizado. Isso usa o JsonObjectRequest(String url, JSONObject jsonRequest, Listener<JSONObject> listener, ErrorListener errorlistener)construtor. RequestFuture<JSONObject>implementa as interfaces Listener<JSONObject>e ErrorListener, para que possa ser usado como os dois últimos parâmetros.
Matt
21
está bloqueando para sempre!
Mohammed Subhi Sheikh Quroush
9
Dica: pode estar bloqueando para sempre se você chamar future.get () ANTES de adicionar a solicitação à fila de solicitações.
datayeah
3
ele será bloqueado para sempre porque você pode ter um erro de conexão. Leia Blundell Resposta
Mina Gabriel
4
Deve-se dizer que você não deve fazer isso no thread principal. Isso não ficou claro para mim. Porque, se o segmento principal estiver bloqueado pelo future.get()aplicativo, o aplicativo será interrompido ou atingirá o tempo limite, com certeza, se definido.
#
125

Nota A resposta do @Matthews está correta, MAS se você estiver em outro segmento e fizer uma chamada de vôlei quando não tiver Internet, seu retorno de erro será chamado no segmento principal, mas o segmento em que você estiver será bloqueado PARA SEMPRE. (Portanto, se esse segmento for um IntentService, você nunca poderá enviar outra mensagem para ele e seu serviço estará basicamente morto).

Use a versão get()que tem um tempo limite future.get(30, TimeUnit.SECONDS)e pegue o erro para sair do seu encadeamento.

Para corresponder à resposta de @Mathews:

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

Abaixo, envolvi-o em um método e use uma solicitação 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
fonte
Esse é um ponto bastante importante! Não sei por que essa resposta não recebeu mais votos.
Jedidja #
1
Também é importante notar que, se você passar a ExecutionException para o mesmo ouvinte que você passou na solicitação, estará processando a exceção duas vezes. Essa exceção ocorre quando ocorreu uma exceção durante a solicitação pela qual o voleio passará para o errorListener para você.
Stimsoni
@ Blundell Não entendo sua resposta. Se o ouvinte for executado no encadeamento da interface do usuário, você terá um encadeamento em segundo plano em espera e o encadeamento da interface do usuário que chama notifyAll (). Um impasse pode ocorrer se a entrega for feita no mesmo encadeamento que você está bloqueado no futuro get (). Portanto, sua resposta parece sem nenhum sentido.
Greywolf82
1
@ greywolf82 IntentServiceé um executor pool de threads de um único segmento, portanto, que IntentService será bloqueado foreveras ele fica em um loop
Blundell
@ Blundell eu não entendo. Ele fica até que uma notificação seja chamada e será chamada do thread da interface do usuário. Até que você tenha dois thread diferente Eu não posso ver o impasse
greywolf82
9

Provavelmente, é recomendável usar o Futuro, mas se por qualquer motivo você não quiser, em vez de preparar sua própria coisa de bloqueio sincronizado, use a java.util.concurrent.CountDownLatch. Então isso funcionaria assim ..

//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 as pessoas pareciam realmente tentar fazer isso e tiveram alguns problemas, decidi que eu realmente forneceria uma amostra da "vida real" disso em uso. Aqui está https://github.com/timolehto/SynchronousVolleySample

Agora, embora a solução funcione, ela tem algumas limitações. Mais importante, você não pode chamá-lo no thread principal da interface do usuário. O Volley executa as solicitações em segundo plano, mas, por padrão, o Volley usa o principal Looperdo aplicativo para despachar as respostas. Isso causa um impasse, pois o thread principal da interface do usuário está aguardando a resposta, mas Looperestá aguardando onCreatea conclusão antes de processar a entrega. Se você realmente quiser fazer isso, poderá, em vez dos métodos auxiliares estáticos, instanciar o seu próprio RequestQueuepassando-o ExecutorDeliveryvinculado a um Handleruso de um Looperque está vinculado a um thread diferente do thread principal da interface do usuário.

Timo
fonte
Esta solução bloqueia meu fio para sempre, mudou Thread.sleep vez de CountDownLatch e problema resolvido
snersesyan
Se você pode fornecer uma amostra completa de um código com falha dessa maneira, talvez possamos descobrir qual é o problema. Não vejo como dormir em conjunto com as travas da contagem regressiva faz sentido.
Timo
Tudo bem @VinojVetha Atualizei a resposta um pouco para esclarecer a situação e forneci um repositório do GitHub que você pode facilmente clonar e experimentar o código em ação. Se você tiver mais problemas, forneça um garfo do repositório de amostra demonstrando o seu problema como referência.
Timo
Essa é uma ótima solução para solicitação síncrona até o momento.
bikram 23/03
2

Como uma observação complementar para as respostas @Blundells e @Mathews, não tenho certeza de que nenhuma ligação seja entregue a nada além do segmento principal de Volley.

A fonte

Observando a RequestQueueimplementação , parece que você RequestQueueestá usando a NetworkDispatcherpara executar a solicitação e a ResponseDeliverypara entregar o resultado (o que ResponseDeliveryé injetado no NetworkDispatcher). Por ResponseDeliverysua vez, ele é criado com um Handlerspawn a partir do encadeamento principal (algo em torno da linha 112 na RequestQueueimplementação).

Em algum lugar da linha 135 na NetworkDispatcherimplementação , parece que também resultados bem-sucedidos são entregues da mesma maneira ResponseDeliveryque qualquer erro. Novamente; um ResponseDeliverybaseado em uma Handlersemente do segmento principal.

Fundamentação

Para o caso de uso em que uma solicitação deve ser feita IntentService, é justo supor que o encadeamento do serviço deve bloquear até recebermos uma resposta do Volley (para garantir um escopo de tempo de execução de vida para lidar com o resultado).

Soluções sugeridas

Uma abordagem seria substituir a maneira padrão como a RequestQueueé criada , onde um construtor alternativo é usado, injetando uma ResponseDeliveryque gera do encadeamento atual e não do encadeamento principal. Eu não investiguei as implicações disso, no entanto.

dbm
fonte
1
A implementação de uma implementação ResponseDelivery personalizada é complicada pelo fato de o finish()método na Requestclasse e na RequestQueueclasse serem pacotes privados, além de usar um hack de reflexão, não tenho certeza de que haja uma maneira de contornar isso. O que acabei fazendo para impedir que qualquer coisa em execução no thread principal (UI) foi configurar um thread Looper alternativo (usando Looper.prepareLooper(); Looper.loop()) e transmitir uma ExecutorDeliveryinstância ao RequestQueueconstrutor com um manipulador para esse looper. Você tem a sobrecarga de outro looper, mas ficar fora do segmento principal
Stephen James Hand
1

Eu uso um bloqueio para alcançar esse efeito agora estou me perguntando se está correto meu caminho alguém quer 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();
            }
        }
força de vontade
fonte
Eu acho que você está implementando essencialmente o que o Volley já fornece quando usa "futuros".
spaaarky21
1
Eu recomendaria ter o catch (InterruptedException e)interior do loop while. Caso contrário fio vai deixar de esperar se interrompida por algum motivo
jayeffkay
@jayeffkay Eu já estou capturando a exceção se ocorrer uma ** InterruptedException ** no loop while, a captura lida com isso.
forcewill
1

Quero acrescentar algo à resposta aceita de Matthew. Embora RequestFuturepossa parecer fazer uma chamada síncrona a partir do encadeamento que você a criou, ela não faz. Em vez disso, a chamada é executada em um encadeamento em segundo plano.

Pelo que entendi depois de percorrer a biblioteca, os pedidos no RequestQueuesão despachados em seu start()método:

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

Agora as classes CacheDispatchere NetworkDispatcherestendem o thread. Com tanta eficiência, um novo encadeamento de trabalho é gerado para desenfileirar a fila de solicitações e a resposta é retornada aos ouvintes de sucesso e erro implementados internamente por RequestFuture.

Embora seu segundo objetivo seja atingido, mas você não é o primeiro, uma vez que sempre é gerado um novo encadeamento, não importa de qual segmento você executa RequestFuture .

Em resumo, a solicitação síncrona verdadeira não é possível com a biblioteca Volley padrão. Corrija-me se eu estiver errada.

Vignatus
fonte
0

Você pode fazer uma solicitação de sincronização com o voleio, mas deve chamar o método em um segmento diferente ou o aplicativo em execução bloqueará, deve ser assim:

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;


}

Depois disso, você pode chamar o método no thread:

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

                                        String response = syncCall();

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