I've attempted to search for any discussion on this topic, but I haven't found anything useful thus far. Therefore, I decided to go ahead and post this.
So my query is about Android best practices. I am creating a simple app that calls a RESTful end point, parses the downloaded JSON, and shows the results in some fragments contained within an activity.
I have a custom "utility" class that extends AsyncTask. In the doInBackground method, I make the request, store the response in a String, etc. (pretty simple stuff).
Now, I understand there are two other methods part of AsyncTask - onPreExecute and onPostExecute. If what I have researched online is correct, these methods are where I should be able to interact with the UI, by passing my context to this utility class, finding the view by id, setting the field text, or whatever it is that I want to do.
So that's my question is about. What is the best practice surrounding this?
Currently, my utility class that extends AsyncTask and makes the Web service call has a private member variable:
private FragmentActivity mActivityContext;
And then, to interact with the UI, I do something like this:
@Override
protected void onPreExecute()
{
super.onPreExecute();
android.support.v4.app.FragmentManager fm = mActivityContext.getSupportFragmentManager();
MainActivity.PlaceholderFragment fragment = (MainActivity.PlaceholderFragment)fm.findFragmentByTag("PlaceholderFragment");
if (fragment != null)
{
TextView textView = (TextView)fragment.getView().findViewById(R.id.my_custom_field);
textView.setText("This field was updated through onPreExecute!");
}
}
Although this seems to work okay so far, my concern is - should the utility class be accessing the fragment manager of the activity it has a reference to? Should it even have a reference to the activity? And by using the fragment manager, the utility class has to know the fragment tag. This isn't a very modular or loosely-coupled approach. Is the solution as simple as just passing in the fragment tag when I create the instance of the utility class instead of having it hard set like I do now? Or is there a better alternative route?
This is what I do, I learned that from StackOverflow:
Lets say we are downloading categories from the server database
First, create an interface
public interface OnCategoriesDownloadedListener {
public void onCategoriesDownloadSuccess(ArrayList<String> categories);
public void onCategoriesDownloadFailure(String reason);
}
then, in the activity that is waiting for those categories to be downloaded, I implement this interface:
public class MyActivity extends Activity implements OnCategoriesDownloadedListener {
@Override
onCategoriesDownloadSuccess(ArrayList<String> categories) {
//get the categories and do whatever my soul desire
}
@Override
onCategoriesDownloadFailure(String reason) {
//get the reason and come up with appropriate failure excuse
}
}
in my asynctask class, I do the following
public class GetCategoriesFromServerAsync extends AsyncTask<String, String, ArrayList<String>> {
private OnCategoriesDownloadedListener listener;
private ArrayList<String> categoryNamesArray;
private String reason;
public GetCategoriesFromServerAsync(OnCategoriesDownloadedListener listener) {
this.listener = listener;
//usually I pass more data here, for example when Im registering a user in the server database, Im using the constructor to pass their data.
}
@Override
protected ArrayList<String> doInBackground(String... args) {
int success;
try {
//JSON stuff
return categoryNames;
} else if (success == 2) {
reason = "failure";
return null;
} else {
reason = "Connection error";
return null;
}
} catch (JSONException e) {
e.printStackTrace();
reason = "Connection error";
return null;
}
}
@Override
protected void onPostExecute(Object[] categoryNamesAndLogos) {
if (listener != null) {
if (categoryNames!=null) {
listener.onCategoriesDownloadSuccess(categoryNames);
} else if (reason.contentEquals(TAG_FAILURE)) {
listener.onCategoriesDownloadFailure(reason);
} else if (reason.contentEquals(TAG_CONNECTION_ERROR)) {
listener.onCategoriesDownloadFailure(reason);
}
}
super.onPostExecute(categoryNames);
}
}
From the activity that implements the listener I start the asynctask, using its constructor:
new GetCategoriesFromDatabase(this).execute();
thus noting that that activity is implementing the listener and is awaiting the results.
In the asynctask postExecute I just deal with the results, send them to the activity that is waiting for them and deal with them in the onCategoriesDownloadSuccess method
If you want to display a Loader, you can do that alongside starting the asynctask from your activity and end displaying it in the onSuccess and onFailure methods. At this point Im doing this in the onPreExecute and onPostExecute of the AsyncTask, but I gotta take some time or advice and think about which is the better approach.
Not sure whether this is a best practice, because its the only one I know but it certainly works for me.
@J. Kowalski: This is indeed a good practice, although onPost and onPre AsyncTasks methods are executed on UI thread, its better to do this way.
Just a small edit, don't pass activity/interface instance in the task constructor, rather use onResume and onPause of activity to set and remove (nullify) the interface reference to the AsyncTask, that way you can even save the response, for later use if the activity goes in pause state while the task is running, and GC can flush the activity objects, if the task is taking too much time.
Pseudo Code
AsyncTask
public class GetCategoriesFromServerAsync extends AsyncTask<String, String, ArrayList<String>> {
private OnCategoriesDownloadedListener listener;
private ArrayList<String> categoryNamesArray;
private String reason;
public GetCategoriesFromServerAsync() {
//usually I pass more data here, for example when Im registering a user in the server database, Im using the constructor to pass their data.
}
@Override
protected ArrayList<String> doInBackground(String... args) {
int success;
try {
//JSON stuff
return categoryNames;
} else if (success == 2) {
reason = "failure";
return null;
} else {
reason = "Connection error";
return null;
}
} catch (JSONException e) {
e.printStackTrace();
reason = "Connection error";
return null;
}
}
@Override
protected void onPostExecute(Object[] categoryNamesAndLogos) {
if (listener != null) {
if (categoryNames!=null) {
listener.onCategoriesDownloadSuccess(categoryNames);
} else if (reason.contentEquals(TAG_FAILURE)) {
listener.onCategoriesDownloadFailure(reason);
} else if (reason.contentEquals(TAG_CONNECTION_ERROR)) {
listener.onCategoriesDownloadFailure(reason);
}
} else {
//listener is null, activity gone in onPause, either save to file, or run the task again
}
super.onPostExecute(categoryNames);
}
public void setInterface(OnCategoriesDownloadedListener listener) {
this.listener = listener;
}
}
Activity
public class SampleActivity extends Activity {
GetCategoriesFromServerAsync mTask;
boolean isDataLoaded = false;
protected void onResume() {
if(mTask != null) {
switch(mTask.getStatus()) {
case Status.PENDING:
case Status.RUNNING:
mTask.setInterface(this);
break;
case Status.FINISHED:
if(isDataLoaded) {
//success case, do nothing...
} else {
//load from file, or start the task again
}
break;
}
}
}
protected void onPause() {
if(mTask != null) mTask.setInterface(null);
}
}
Hope that helps.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With