65

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.

Michael
  • 22,196
  • 33
  • 132
  • 187
  • 2
    Android network activity cannot run on the Main thread. I have no experience with Okhttp, but I am pretty sure you are on a separate thread. – TmKVU Jun 16 '14 at 15:18
  • I thought Okhttp handles network io on a separate thread and callbacks back on the main thread. At least that's what goes for Retrofit according to @jake-wharton one of the creators http://stackoverflow.com/a/21010181/599912 – Michael Jun 16 '14 at 15:31
  • I see. I found a `Calback` class in their [documentation](http://square.github.io/okhttp/javadoc/com/squareup/okhttp/Callback.html). You can try to implement that one. – TmKVU Jun 16 '14 at 15:47
  • @TmKVU I actually implement it in the anonymous class in `client.newCall(request).enqueue()`. I have never spawned another thread off the main thread so I suspect the Okhttp does it inherently. I think that somehow control is not being transfered back to the main thread – Michael Jun 16 '14 at 16:06

6 Answers6

79

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.

Jake Wharton
  • 75,598
  • 23
  • 223
  • 230
  • Thanks, indeed I had to run the callback on the main thread. Can you have a look at the piece of code I added to the answer below ? Not sure if this is the right way to do that, how would you go about it ? Does Okhttp codebase contain Android specific example ? – Michael Jun 16 '14 at 21:14
  • 2
    The problem now is that a call to `response.body().string()` in MainActivity throws `android.os.NetworkOnMainThreadException` (because it's being executed on the main thread). Does this call do any network op ? – Michael Jun 16 '14 at 21:39
  • I don't see a big reason why the callbacks should be in the background threads and not on UI, since in most cases, we'd be executing the network calls wanting them to run in background but post their results on the front end/UI – Aman Alam Feb 16 '15 at 13:43
  • Okay, just read on the github issue page that since the way okhttp works, whatever response is received, is given to the callbacks to 'read' from, and since it won't know how big the response is, and how long it will take to read it, it all runs in the background, and assumes that higher level libraries like retrofit will handle the rest of the cases. Valid, but still not too convinced. This makes okhttp a little less easier to use as a standalone network library – Aman Alam Feb 16 '15 at 14:51
  • 20
    OkHttp knows nothing about the main thread. It's not an Android library, it's a Java library. Furthermore, by calling you back on the initial data it allows streaming responses and potentially incrementally updating the UI before the entire response has been consumed. – Jake Wharton Feb 16 '15 at 19:20
  • 9
    " It's not an Android library, it's a Java library" - Makes much more sense now. Thanks! – Aman Alam Feb 17 '15 at 17:12
46

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);
                    }
                });

            }
        });
 }
Michael
  • 22,196
  • 33
  • 132
  • 187
  • where is your `context` come from.when a activity call the the `HttpUtil` must to be introduction its `context`? which i think it is not very well. – chinaanihchen May 15 '15 at 02:47
  • 5
    you could try `new Handler(Looper.getMainLooper())` instead of `new Handler(context.getMainLooper())` – chip Aug 17 '15 at 04:27
  • 2
    are you not having issue with this helper on configuration change of a fragment or activity as cb would be referencing to old object before configuration change? – MohK Sep 07 '15 at 09:37
20

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);                
            }
        });
   }
});
Marcos Casagrande
  • 37,983
  • 8
  • 84
  • 98
1

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

  • 2
    This question is about OkHttp, not Retrofit. Retrofit handles the switch back to the main thread automatically, OkHttp doesn't. – Jim E-H Oct 07 '19 at 19:08
1

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){

                }
            }
        }
    });
 }
Adam Davis
  • 379
  • 5
  • 16
1

What i would do is extend okhttp callback and call failure / response on main thread so caller don't have to care about posting them on main thread.

Call call;
call.enqueue(new OkHttpCallbackOnMain() {
    @Override
    void onCallFailure(Call call, IOException e) {
        // update UI
    }

    @Override
    void onCallResponse(Call call, Response response) {
        // update UI
    }
});

abstract class OkHttpCallbackOnMain implements Callback {

    abstract void onCallFailure(Call call, IOException e);

    abstract void onCallResponse(Call call, Response response));


    @Override
    public void onFailure(@NonNull Call call, @NonNull IOException e) {
        getMainHandler().post(() -> onCallFailure(call, e));
    }

    @Override
    public void onResponse(@NonNull Call call, @NonNull Response response)  {
        getMainHandler().post(() -> onCallResponse(call, response));
    }

    private Handler getMainHandler() {
        return new Handler(Looper.getMainLooper());
    }
}
H4SN
  • 1,482
  • 3
  • 24
  • 43