75

I'm switching to Retrofit and trying to understand proper architecture for using it with async callbacks.

For example I have an interface:

interface RESTService{
    @GET("/api/getusername")
    void getUserName(@Query("user_id") String userId, 
                     Callback<Response> callback);
}

And I run this from main activity:

RestAdapter restAdapter = new RestAdapter.Builder()
        .setServer("WEBSITE_URL")     
        .build();
RESTService api = restAdapter.create(RESTService.class);
api.getUserName(userId, new Callback<Response> {...});

Then user rotates the device and I have newly created activity... What was happen here? How can I get response to the new activity (I assume that api call in background will execute longer than first activity life). Maybe I must use static instance of callback or what? Please show me the right way...

moud
  • 729
  • 9
  • 19
lordmegamax
  • 2,684
  • 2
  • 26
  • 29

7 Answers7

42

Use otto. There are a lot of samples to mix otto and retrofit, for example https://github.com/pat-dalberg/ImageNom/blob/master/src/com/dalberg/android/imagenom/async/FlickrClient.java

Or read this post http://www.mdswanson.com/blog/2014/04/07/durable-android-rest-clients.html It answers on almost all questions

nikib3ro
  • 20,366
  • 24
  • 120
  • 181
avgx
  • 644
  • 5
  • 8
  • Thanks for another way! What are global differences between Otto and RoboSpice? Both use listeners, POJOs, request classes... How can I cancel web request when activity is paused? – lordmegamax Apr 16 '14 at 08:16
  • I haven't used robospice. I use things from square. If your activity paused you remove the subscription to Bus in onPause method. So nothing will be broken. – avgx Apr 22 '14 at 18:54
  • 14
    This is in no way an answer to OP's question which was "How can I get response to the new activity". The blog post explicitly says that "If an activity initiated an API call and then gets destroyed or backgrounded, we will still post the resulting response event, but no one will be there to listen. ... We are always re-querying data whenever our activity is resumed" – Gabor Sep 18 '14 at 09:21
  • @Gabor The OP is asking about device rotation. In this case the activity re-subscribes and gets the response. If it's too late, there can be a producer on the other end that can cache the response. Finally, re-querying the data issue should be handled via the savedInstanceState. – black Feb 14 '16 at 16:04
  • You would need `setRetainInstance` and `Observable.cache` for that. Here is an example I wrote, pretty much copying an RxAndroid sample: https://github.com/ber4444/u2020-v2/blob/master/Application/src/main/java/com/gabor/recyclerview/RecyclerViewFragment.java – Gabor Feb 16 '16 at 18:38
  • @Gabor: No, you would need neither of those to handle resubscribtion across Activity restarts, since the main thread message queue processing is suspended in the interim. Just resubscribing to the event on the recreation of the Activity is sufficient. – corsair992 Apr 11 '16 at 11:01
  • that's when the OS decides NOT to kill the Activity, which is not guaranteed – Gabor Apr 11 '16 at 18:57
  • @Gabor: I'm not sure what you mean by the OS deciding to kill the Activity. It's already being killed and restarted by the system in the scenario we're discussing, and this answer describes how to handle that scenario. – corsair992 Apr 11 '16 at 19:16
34

For potential long running server calls i use an AsyncTaskLoader. For me, the main advantage of Loaders are the activity-lifecycle handling. onLoadFinished is only called if your activity is visible to the user. Loaders are also shared between activity/fragment and orientation changes.

So i created an ApiLoader which uses retrofits synchronous calls in loadInBackground.

abstract public class ApiLoader<Type> extends AsyncTaskLoader<ApiResponse<Type>> {

    protected ApiService service;
    protected ApiResponse<Type> response;

    public ApiLoader(Context context) {
        super(context);
        Vibes app = (Vibes) context.getApplicationContext();
        service = app.getApiService();
    }

    @Override
    public ApiResponse<Type> loadInBackground() {
        ApiResponse<Type> localResponse = new ApiResponse<Type>();

        try {
            localResponse.setResult(callServerInBackground(service));
        } catch(Exception e) {
            localResponse.setError(e);
        }

        response = localResponse;
        return response;
    }

    @Override
    protected void onStartLoading() {
        super.onStartLoading();
        if(response != null) {
            deliverResult(response);
        }

        if(takeContentChanged() || response == null) {
            forceLoad();
        }
    }

    @Override
    protected void onReset() {
        super.onReset();
        response = null;
    }


    abstract protected Type callServerInBackground(SecondLevelApiService api) throws Exception;

}

In your activity you init this loader like this:

getSupportLoaderManager().initLoader(1, null, new LoaderManager.LoaderCallbacks<ApiResponse<DAO>>() {
        @Override
        public Loader<ApiResponse<DAO>> onCreateLoader(int id, Bundle args) {
            spbProgress.setVisibility(View.VISIBLE);

            return new ApiLoader<DAO>(getApplicationContext()) {
                @Override
                protected DAO callServerInBackground(ApiService api) throws Exception {
                    return api.requestDAO();
                }
            };
        }

        @Override
        public void onLoadFinished(Loader<ApiResponse<DAO>> loader, ApiResponse<DAO> data) {
            if (!data.hasError()) {
                DAO dao = data.getResult();
                //handle data
            } else {
                Exception error = data.getError();
                //handle error
            }
        }

        @Override
        public void onLoaderReset(Loader<ApiResponse<DAO>> loader) {}
    });

If you want to request data multiple times use restartLoader instead of initLoader.

Benjamin
  • 451
  • 3
  • 6
  • 5
    I have created a full example based on your idea: https://github.com/rciovati/retrofit-loaders-example – rciovati Jul 25 '14 at 14:22
  • Watch the Google IO 2013 Volley presentation by a Google engineer, Loaders are bad for network requests. – JBeckton Sep 27 '15 at 19:02
  • @JBeckton: Looking at the [transcript](http://transcriptvids.com/v2/yhv8l9F44qo.html) of the Volley presentation, I don't see any mention of the Loader framework (only the "image loader" from Volley)… – corsair992 Oct 04 '15 at 18:14
  • @Benjamin why you don't use `Service` or `IntentService` instead of `AsyncTask` ? – mahdi pishguy Oct 13 '16 at 07:14
22

I've been using a kind of MVP (ModelViewPresenter) implementation on my Android apps. For the Retrofit request I made the Activity calls it's respective Presenter, which in turn makes the Retrofit Request and as a parameter I send a Callback with a custom Listener attached to it (implemented by the presenter). When the Callback reach onSuccess or onFailure methods I call the Listener's respective methods, which calls the Presenter and then the Activity methods :P

Now in case the screen is turned, when my Activity is re-created it attaches itself to the Presenter. This is made using a custom implementation of Android's Application, where it keeps the presenters' instance, and using a map for recovering the correct presenter according to the Activity's class.

I don't know if it's the best way, perhaps @pareshgoel answer is better, but it has been working for me.

Examples:

public abstract interface RequestListener<T> {

    void onSuccess(T response);
    
    void onFailure(RetrofitError error);
}

...

public class RequestCallback<T> implements Callback<T> {

    protected RequestListener<T> listener;
    
    public RequestCallback(RequestListener<T> listener){
        this.listener = listener;
    }
    
    @Override
    public void failure(RetrofitError arg0){
        this.listener.onFailure(arg0);
    }

    @Override
    public void success(T arg0, Response arg1){
        this.listener.onSuccess(arg0);
    }

}

Implement the listener somewhere on the presenter, and on the overrode methods call a presenter's method that will make the call to the Activity. And call wherever you want on the presenter to init everything :P

Request rsqt = restAdapter.create(Request.class);
rsqt.get(new RequestCallback<YourExpectedObject>(listener));
peterh
  • 11,875
  • 18
  • 85
  • 108
LeoFarage
  • 517
  • 3
  • 16
10

Firstly, your activity leaks here because this line: api.getUserName(userId, new Callback {...}) creates an anonymous Callback class that holds a strong reference to you MainActivity. When the device is rotated before the Callback is called, then the MainActivity will not be garbage collected. Depending on what you do in the Callback.call(), your app may yield undefined behaviour.

The general idea to handle such scenarios is:

  1. Never create a non-static inner class (or an anonymous class as mentioned in the problem).
  2. Instead create a static class that holds a WeakReference<> to the Activity/Fragment.

The above just prevents Leaks. It still does not help you get the Retrofit call back to your Activity.

Now, to get the results back to your component (Activity in your case) even after configuration change, you may want to use a headless retained fragment attached to your Activity, which makes the call to Retrofit. Read more here about Retained fragment - http://developer.android.com/reference/android/app/Fragment.html#setRetainInstance(boolean)

The general idea is that the Fragment automatically attaches itself to the Activity on configuration change.

pareshgoel
  • 981
  • 8
  • 12
  • 1
    I read about context leaks and [how to avoid them](http://stackoverflow.com/questions/3821423/background-task-progress-dialog-orientation-change-is-there-any-100-working/3821998#3821998). I always can use sync calls in Retrofit without callback inside AsyncTask, set/get activity, check if it isn't null and only then do some work, but how can I proper [use async](http://stackoverflow.com/questions/17034465/how-to-implement-an-async-callback-using-squares-retrofit-networking-library?rq=1) way with callback from Retrofit to do like that or I can't? – lordmegamax Mar 10 '14 at 07:08
5

I highly recommend you watch this video given at Google I/O.

It talks about how to create REST requests by delegating them to a service (which is almost never killed). When the request is completed it is immediately stored into Android's built-in database so the data is immediately available when your Activity is ready.

With this approach, you never have to worry about the lifecycle of the activity and your requests are handled in a much more decoupled way.

The video doesn't specifically talk about retrofit, but you can easily adapt retrofit for this paradigm.

Martin Konecny
  • 57,827
  • 19
  • 139
  • 159
  • 5
    This was 5 years ago, My guess is that the technology has improved since then and using services may not currently be the best way. – JBeckton Sep 27 '15 at 03:00
2

Use Robospice

All components in your app which require data, register with the spice service. The service takes care of sending your request to the server (via retrofit if you want). When the response comes back, all components which registered get notified. If there is one of them not available any more (like an activity which got kicked because of rotation), it's just not notified.

Benefit: One single request which does not get lost, no matter whether you rotate your device, open new dialogs/fragments etc...

stoefln
  • 14,498
  • 18
  • 79
  • 138
2

Using Retrofit2 to handle orientation change. I was asked this in a job interview and was rejected for not knowing it at the time but here it is now.

public class TestActivity extends AppCompatActivity {
Call<Object> mCall;
@Override
    public void onDestroy() {
        super.onDestroy();
        if (mCall != null) {
            if (mCall.isExecuted()) {
                //An attempt will be made to cancel in-flight calls, and
                // if the call has not yet been executed it never will be.
                mCall.cancel();
            }
        }
    }
    }
John61590
  • 1,106
  • 1
  • 13
  • 29
  • 3
    Why do you think we should cancel requests at an activity destroy? – CoolMind Jun 25 '17 at 17:49
  • @CoolMind The request didn't finish, so we start it again in onStart() / onResume(). I didn't add that part, sorry. We aren't using IntentServices here so not "fire and forget." Also, not using headless fragments or AsyncTaskLoaders as well. – John61590 Jul 19 '17 at 08:59
  • You are right, activity's lifecycle is difficult to manage, so there are some architectoral tricks like MVP/MVVM with saving/restoring data, maybe loaders. – CoolMind Jul 23 '17 at 12:25
  • You should find other way than cancelling the call. You can't really cancel every API call because of device rotation. – The Finest Artist Oct 01 '19 at 19:42