Okhttp response callbacks on the main thread
AndroidMultithreadingOkhttpAndroid Problem Overview
I have created a helper class to handle all of my http calls in my app. It is a simple singleton wrapper for okhttp that looks like this (I have omitted some unimportant parts):
public class HttpUtil {
private OkHttpClient client;
private Request.Builder builder;
...
public void get(String url, HttpCallback cb) {
call("GET", url, cb);
}
public void post(String url, HttpCallback cb) {
call("POST", url, cb);
}
private void call(String method, String url, final HttpCallback cb) {
Request request = builder.url(url).method(method, method.equals("GET") ? null : new RequestBody() {
// don't care much about request body
@Override
public MediaType contentType() {
return null;
}
@Override
public void writeTo(BufferedSink sink) throws IOException {
}
}).build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Request request, Throwable throwable) {
cb.onFailure(null, throwable);
}
@Override
public void onResponse(Response response) throws IOException {
if (!response.isSuccessful()) {
cb.onFailure(response, null);
return;
}
cb.onSuccess(response);
}
});
}
public interface HttpCallback {
/**
* called when the server response was not 2xx or when an exception was thrown in the process
* @param response - in case of server error (4xx, 5xx) this contains the server response
* in case of IO exception this is null
* @param throwable - contains the exception. in case of server error (4xx, 5xx) this is null
*/
public void onFailure(Response response, Throwable throwable);
/**
* contains the server response
* @param response
*/
public void onSuccess(Response response);
}
}
Then, in my main activity, I use this helper class :
HttpUtil.get(url, new HttpUtil.HttpCallback() {
@Override
public void onFailure(Response response, Throwable throwable) {
// handle failure
}
@Override
public void onSuccess(Response response) {
// <-------- Do some view manipulation here
}
});
onSuccess
throws an exception when the code runs :
> android.view.ViewRootImpl$CalledFromWrongThreadException: Only the > original thread that created a view hierarchy can touch its views.
From my understanding, Okhttp callbacks run on the main thread so why do I get this error ?
** Just as a side note, I have created HttpCallback
interface to wrap Okhttp's Callback
class because I wanted to change the behaviour of onResponse
and onFailure
so I could unite the logic of handling failed responses due to i/o exception and failed responses due to server problems.
Thanks.
Android Solutions
Solution 1 - Android
> From my understanding, Okhttp callbacks run on the main thread so why do I get this error ?
This is not true. Callbacks run on a background thread. If you want to immediately process something in the UI you will need to post to the main thread.
Since you already have a wrapper around the callback you can do this internally in your helper so that all HttpCallback
methods are invoked on the main thread for convenience.
Solution 2 - Android
As Jake Wharton suggested, I had to run the callbacks on the main thread explicitly.
So I wrapped the calls to the callbacks with Runnable
like this:
private void call(String method, String url, final HttpCallback cb) {
...
client.newCall(request).enqueue(new Callback() {
Handler mainHandler = new Handler(context.getMainLooper());
@Override
public void onFailure(Request request,final Throwable throwable) {
mainHandler.post(new Runnable() {
@Override
public void run() {
cb.onFailure(null, throwable);
}
});
}
@Override
public void onResponse(final Response response) throws IOException {
mainHandler.post(new Runnable() {
@Override
public void run() {
if (!response.isSuccessful()) {
cb.onFailure(response, null);
return;
}
cb.onSuccess(response);
}
});
}
});
}
Solution 3 - Android
I know it's an old question, but recently I encountered the same issue. If you need to update any view, you will need to use runOnUiThread()
or post the result back on the main thread.
HttpUtil.get(url, new Callback() { //okhttp3.Callback
@Override
public void onFailure(Call call, IOException e) { /* Handle error **/ }
@Override
public void onResponse(Call call, Response response) throws IOException {
String myResponse = response.body().string();
//Do something with response
//...
MyActivity.this.runOnUiThread(new Runnable() {
@Override
public void run() {
//Handle UI here
findViewById(R.id.loading).setVisibility(View.GONE);
}
});
}
});
Solution 4 - Android
According to Retrofit documentation Callback methods are executed on UI thread by default until you provide a callback executor to Retrofit OR when using custom method return types using CallAdapterFactory
Solution 5 - Android
Michael's above response is a pretty good solution for having your callback happen in a calling Activity and I used it as a guide for my solution that I will outline below (solution in Kotlin);
Interface for HttpCallback
interface HttpCallback {
fun onResponse(response: Response)
fun onFailure(call: Call, e: IOException)
}
Implementation of the Interface. I pass the calling activity as a weak reference to perform UI updates.
class ImageUploadCallback(activity: WeakReference<Activity>) : HttpCallback {
val _activity = activity
override fun onResponse(response: Response) {
if(_activity.get() != null){
val activty = _activity.get()!!
activty.runOnUiThread(Runnable {
//Update the UI to your hearts content
});
}
}
override fun onFailure(call: Call, e: IOException) {
//Your implemtnation here
}
}
Calling HttpProxy class from the activity, passing the callback as a parameter
HttpProxy.UploadImage(
imageToUpload.name,
imageToUpload,
MEDIA_TYPE!!,
//our callback
ImageUploadCallback(WeakReference(this))
And finally, the code in the HttpProxy class that takes in the callback;
fun UploadImage(filename: String, sourceImageFile: File, mediaType: MediaType, callback: HttpCallback) {
//building up our request...and then calling
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
callback.onFailure(call, e)
}
override fun onResponse(call: Call, response: Response) {
response.use {
try{
callback.onResponse(response)
}
catch(e: Exception){
}
}
}
});
}