AsyncTaskLoader for http requests to handle orientation changes, using generics, inheritance

StackOverflow https://stackoverflow.com/questions/19744022

Вопрос

Currently I am using an asynchronous http library to execute http requests against our server. However this carries the problem where if an http call is in progress during a screen rotation we will have a reference to the old context when the call finishes. I sort of got around this by keeping a static reference to the latest instance captured in onCreate and would call methods with that reference (and null it out in onDestroy). It worked ok but seemed like a hack. I've seen some people recommend the use of fragments to deal with this, like here:

http://www.androiddesignpatterns.com/2013/04/retaining-objects-across-config-changes.html

which seem like good ideas but I was thinking I could accomplish this by simply having my Activity extend FragmentActivity and use an AsyncTaskLoader subclass which is specifically purposed for what I'm doing.

Here is my Idea: Implement an AsyncTaskLoader with takes an ApiRequest and returns an ApiResponse. However I want to be able to subclass HttpAsyncTask and override a method that parses the response so I can parse the response and turn it into another kind of object which extends ApiResponse. I'm not sure how to specify the type arguments to achieve this though.

Here is my code:

public class HttpAsyncTaskLoader</*not sure what to put here*/> extends AsyncTaskLoader<? not sure ?> {
    private ApiClient mClient ;
    private ApiRequest mRequest;
    private volatile boolean isExecuting = false;
    public HttpAsyncTaskLoader(Context context, ApiClient client, ApiRequest request) {
        super(context);
        mClient = client;
        mRequest = request;
    }

    /**
     * Subclasses should override this method to do additional parsing
     * @param response
     * @return
     */
    protected /*subclass of ApiResponse (or ApiResponse itself)*/ onResponse(ApiResponse response) 
    {
        //base implementation just returns the value, subclasses would 
        //do additional processing and turn it into some base class of ApiResponse  
        return response; 
    }

    @Override
    public /** not sure ***/ loadInBackground() {
        HttpResponse response = null;
        ResponseError error = null;
        JSONObject responseJson = null;
        ApiResponse apiResponse = null;
        try {
            isExecuting = true;
            //synchronous call
            response  =  mClient.execute(mRequest);
            isExecuting = false;
            responseJson = new JSONObject(EntityUtils.toString(response.getEntity()));
        } catch (IOException e) {
            error = new ResponseError(e);
        } catch (URISyntaxException e) {
            error = new ResponseError(e);
        } catch (JSONException e) {
            error = new ResponseError(e);
        } finally {
            mClient.getConnectionManager().closeExpiredConnections();
            isExecuting = false;
            apiResponse = new ApiResponse(getContext().getResources(), response, responseJson, error);
        }
        return onResponse(apiResponse);
    }

    @Override
    public void onCanceled(ApiResponse response) {
        if (isExecuting) {
            mClient.getConnectionManager().shutdown();
        }
    }

}

Anyone have an idea how I can accomplish this? I'm not sure how to specify the type parameters? I want this class to be usable as-is hopefully and also to be able to subclass it. The point is that I don't want to re-implement the functionality in the loadInBackground method above. I'm sure I could just use ApiResponse as my generic parameter and then cast the ApiResponse objects returned in onLoadFinished to the specific base class that I'm expecting but I'd rather do this in a more type-safe manner. Also I'm open to ideas that accomplish essentially the same thing but in another way.

Это было полезно?

Решение

Ok this is what I came up with which actually seems to work pretty good and handles screen orientation changes during the background work. Here is my updated HttpAsyncTaskLoader.

public class HttpAsyncTaskLoader<T extends ApiResponse> extends AsyncTaskLoader {
    private ApiClient mClient ;
    protected ApiRequest mRequest;
    private ApiResponse mResponse;
    private volatile boolean isExecuting = false;
    public HttpAsyncTaskLoader(Context context, ApiClient client, ApiRequest request) {
        super(context);
        mClient = client;
        mRequest = request;
    }

    /** Subclasses should call this from loadInBackground   */
    protected ApiResponse executeRequest(ApiRequest request) {
        HttpResponse response = null;
        ResponseError error = null;
        JSONObject responseJson = null;
        try {
            isExecuting = true;
            Log.d(TAG, "executing api");
            response  =  mClient.execute(request);
            Log.d(TAG, "got a response");
            isExecuting = false;
            responseJson = new JSONObject(EntityUtils.toString(response.getEntity()));
            Log.d(TAG, "parsed response to json");
        } catch (IOException e) {
            error = new ResponseError(e);
        } catch (URISyntaxException e) {
            error = new ResponseError(e);
        } catch (JSONException e) {
            error = new ResponseError(e);
        } finally {
            mClient.getConnectionManager().closeExpiredConnections();
            isExecuting = false;
            mResponse = new ApiResponse(getContext().getResources(), response, responseJson, error);
        }
        return mResponse;
    }

    protected void onStartLoading() {
        super.onStartLoading();
        if (takeContentChanged() ||  mResponse == null) {
            forceLoad();
        }
        if (getResponse() != null) {
            deliverResult(getResponse());
        }
    }

    /** 
    * Subclasses should also override this so the correct object 
    * gets delivered in all cases (see onStartLoading above)
    */
    public ApiResponse getResponse() {
        return mResponse;
    }

    @Override
    public void onCanceled(Object data) {
        super.onCanceled(data);
        if (isExecuting) {
            mClient.getConnectionManager().shutdown();
        }
    }

    @Override
    public ApiResponse loadInBackground() {
        return executeRequest(mRequest);
    }
}

Note that in the above example the onCanceled method takes an Object. I got compile errors if I attempted to use ApiResponse. as the type. Also, you must implement onStartLoading like I did above (calling forceLoad if the result object is null) or else loadInBackground won't get called

Then here is an example of a subclass of HttpAsyncTaskLoader:

public class LoginAsyncTaskLoader extends HttpAsyncTaskLoader {
    private LoginResponse mLoginResponse;
    public LoginAsyncTaskLoader(Context context, ApiClient client, ApiRequest request) {
        super(context, client, request);
    }

    @Override
    public LoginResponse loadInBackground() {
        ApiResponse apiResponse = executeRequest(mRequest);
        mLoginResponse = new LoginResponse(apiResponse.getResources(), apiResponse.response, apiResponse.responseJson, apiResponse.getError());
        return mLoginResponse;
    }

    @Override
    public ApiResponse getResponse() {
        return mLoginResponse;
    }
}

Here is an Activity that uses this loader:

public class LoginActivity extends FragmentActivity implements LoaderManager.LoaderCallbacks<LoginResponse> {

    private String username,password;       
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // TODO Auto-generated method stub
        super.onCreate(savedInstanceState);
        setContentView(R.layout.login);
        Loader loader = getSupportLoaderManager().getLoader(0);
        if (loader != null) {
            getSupportLoaderManager().initLoader(0, null, this);
        }
    }

    public void loginSubmit(View button) {
            Bundle data = new Bundle();
            data.putString("username", getUsername());
            data.putString("password", getPassword());  
            getSupportLoaderManager().restartLoader(0, data, this);
    }   


    @Override
    public Loader<LoginResponse> onCreateLoader(int i, Bundle bundle) {
    //might want to start a progress bar
        ApiClient client = new ApiClient();
        LoginApi loginApi = new LoginApi(bundle.getString("username"), bundle.getString("password"));
        return new LoginAsyncTaskLoader(this, apiClient, loginApi);
    }


    @Override
    public void onLoadFinished(Loader<LoginResponse> loginLoader,
                               LoginResponse loginResponse)
    {
        //handle result, maybe send to a new activity if response doesn't have an error

    }

    @Override
    public void onLoaderReset(Loader<LoginResponse> responseAndJsonHolderLoader)
    {
        //not sure if anything needs to be done here to do

    }
}

Note that while this loader doesn't start until the user presses the Login button, You must reconnect to the loader using initLoader in onCreate in case it was already in progress, otherwise when you flip orientations you won't get notified that the task finished.

Interesting that this seems to work good and doesn't require using a TaskFragment. I haven't really seen anyone else do this for http stuff so maybe there are some down sides but it seems to work just fine.

Другие советы

Are you not interested in trying to implement libraries dedicated to this kind of problems? You Have Volley by Google and Robospice for example.

http://arnab.ch/blog/2013/08/asynchronous-http-requests-in-android-using-volley/

https://github.com/octo-online/robospice

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top