Best practice to implement Retrofit callback to recreated activity?

AndroidWeb ServicesRestRetrofit

Android Problem Overview


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

Android Solutions


Solution 1 - Android

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

Solution 2 - Android

For potential long running server calls i use an [AsyncTaskLoader][1]. 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. [1]: http://developer.android.com/guide/components/loaders.html

Solution 3 - Android

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

Solution 4 - Android

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.

Solution 5 - Android

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.

Solution 6 - Android

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

Solution 7 - Android

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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionlordmegamaxView Question on Stackoverflow
Solution 1 - AndroidavgxView Answer on Stackoverflow
Solution 2 - AndroidBenjaminView Answer on Stackoverflow
Solution 3 - AndroidLeoFarageView Answer on Stackoverflow
Solution 4 - AndroidpareshgoelView Answer on Stackoverflow
Solution 5 - AndroidMartin KonecnyView Answer on Stackoverflow
Solution 6 - AndroidstoeflnView Answer on Stackoverflow
Solution 7 - AndroidJohn61590View Answer on Stackoverflow