Hi there I'm thinking about what is the correct and best way to handle Activity, Fragment, AsyncTask and DialogFragments together.
My current state is that I start my Activity and replace its ContentView with my Fragment, in which I got an EditText and one Button.
Tapping my Button executes an AsyncTasks which Requests random things and takes some time. Meanwhile I display a DialogFragment begging for patience.
Desired behavior is that, e.g. I rotate my screen my DialogFragment keeps being displayed for the time my AsyncTask is running. After that I want to show up a simple toast displaying the information I got from my HttpRequest.
Compact overview about how I thought it would work:
BaseFragment keeps a WeakReference to the Activity it's attached to
AsyncTask keeps a WeakReference to Fragment which exectures it
AsyncTasks onPreExecute() shows up the DialogFragment
AsyncTasks onPostExecute() dissmisses the DialogFragment
BaseFragment holds DialogFragment
Unfortunately this is not the way it works, on orientation change my DialogFragment keeps being displayed and no toast is showing up.
What am I doing wrong ?
public class BaseFragment extends Fragment{
private static final String TAG = BaseFragment.class.getSimpleName();
protected WeakReference<AppCompatActivity> mActivity;
private TemplateDialogFragment dialogFragment;
public WeakReference<AppCompatActivity> getAppCompatActivity(){ return mActivity; }
#Override
public void onAttach(Context context) {
if(!(context instanceof AppCompatActivity)) {
throw new IllegalStateException(TAG + " is not attached to an AppCompatActivity.");
}
mActivity = new WeakReference<>((AppCompatActivity) context);
super.onAttach(context);
}
#Override
public void onDetach() {
mActivity = null;
super.onDetach();
}
#Override
public void onStart() {
super.onStart();
showContent();
}
public void showContent(){
}
public void showDialog(String title, String content){
dialogFragment = new TemplateDialogFragment();
Bundle bundle = new Bundle();
bundle.putString(TemplateDialogFragment.DIALOG_TITLE, title);
bundle.putString(TemplateDialogFragment.DIALOG_MESSAGE, content);
dialogFragment.setArguments(bundle);
dialogFragment.show(getFragmentManager(), TemplateDialogFragment.FRAGMENT_TAG);
}
public void notifyTaskFinished(String result) {
dismissDialog();
if(mActivity != null && !mActivity.get().isFinishing()) {
Toast.makeText(mActivity.get().getApplicationContext(), result, Toast.LENGTH_LONG).show();
}
}
private void dismissDialog(){
if(dialogFragment != null && dialogFragment.isAdded()) {
dialogFragment.dismissAllowingStateLoss();
}
}
}
...
public class TemplateFragment extends BaseFragment {
private static final String TAG = TemplateFragment.class.getSimpleName();
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.test_fragment, container, false);
}
#Override
public void showContent() {
super.showContent();
Button startTask = (Button) getAppCompatActivity().get().findViewById(R.id.button0);
final BaseFragment instance = this;
startTask.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
CustomAsyncTask task = new CustomAsyncTask(instance);
EditText input = (EditText) getAppCompatActivity().get().findViewById(R.id.text0);
task.execute(input.getText().toString());
}
});
}
private static class CustomAsyncTask extends AsyncTask<String, Void, String> {
WeakReference<BaseFragment> weakBaseFragmentReference;
private CustomAsyncTask(BaseFragment fragment) {
weakBaseFragmentReference = new WeakReference<>(fragment);
}
#Override
protected void onPreExecute() {
super.onPreExecute();
weakBaseFragmentReference.get().showDialog("Executing", "Working on the request...");
}
#Override
protected String doInBackground(String... params) {
HttpURLConnection con = HttpUrlConnectionFactory.createUrlConnection("https://www.httpbin.org/bytes/" + (params[0] == null ? "1" : params[0]));
return HttpRequester.doGet(con).getResponseAsString();
}
#Override
protected void onPostExecute(String response) {
super.onPostExecute(response);
if(weakBaseFragmentReference.get() == null) {
return;
}
weakBaseFragmentReference.get().notifyTaskFinished(response);
}
}
}
*Edit:
After some time researching this theme I'm sure a Service is the best solution for most of my field of use. Also I used AsyncTaskLoaders a lot, because there is a smooth control of lifecycle....
Use progress bar instead of DialogFragment.
AsyncTask should only be used for tasks that take quite few seconds.
AsyncTask doesn't respect Activity lifecycle, and can lead to memory leaks.
Check some gotchas.
You can try AsyncTaskLoader to survive configuration changes.
Related
I've an AppCompatActivity that uses the NavigationDrawer pattern, managing some fragments. In one of these, that has no setRetainInstance(true), I show a DialogFragment with a ProgressDialog inside and an AsyncTask with this code:
SavingLoader savingLoader = SavingLoader.newInstance(savingLoaderMaxValue);
savingLoader.show(getChildFragmentManager(), SAVING_LOADER_TAG);
new MyAsyncTask().execute();
Where the SavingLoader class is this one:
public class SavingLoader extends DialogFragment {
private static final String MAX_VALUE_TAG = "MAX_VALUE_TAG";
private static final String PROGRESS_VALUE_TAG = "PROGRESS_VALUE_TAG";
public static SavingLoader newInstance(int max_value){
SavingLoader s = new SavingLoader();
Bundle args = new Bundle();
args.putInt(MAX_VALUE_TAG, max_value);
s.setArguments(args);
return s;
}
private ProgressDialog dialog;
public SavingLoader(){}
#Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setCancelable(false);
}
#NonNull
#Override
public Dialog onCreateDialog(Bundle savedInstanceState){
dialog = new ProgressDialog(getActivity(), getTheme());
dialog.setTitle(getString(R.string.dialog_title_saving));
dialog.setMessage(getString(R.string.dialog_message_saving));
dialog.setIndeterminate(false);
int max = (savedInstanceState == null ?
getArguments().getInt(MAX_VALUE_TAG) : savedInstanceState.getInt(MAX_VALUE_TAG));
if (max >= 1){
dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
dialog.setProgress((savedInstanceState == null ?
0 : savedInstanceState.getInt(PROGRESS_VALUE_TAG)));
dialog.setMax(max);
} else dialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
return dialog;
}
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(MAX_VALUE_TAG, dialog.getMax());
outState.putInt(PROGRESS_VALUE_TAG, dialog.getProgress());
}
public int getProgress(){
return dialog.getProgress();
}
public int getMax(){
return dialog.getMax();
}
public void incrementProgressBy(int value){
if (dialog.getProgress() + value <= dialog.getMax())
dialog.incrementProgressBy(value);
}
}
In the onPostExecute() method I need to perform some UI update so here's my problem: if I start the dialog and the AsyncTask (like above) and I don't rotate my phone, all works as expected. Same thing if I rotate phone AFTER the onPostExecute() method. But if I rotate my phone WHILE the AsyncTask is still running, when it completes and reach the onPostExecute() method it gives me the IllegalStateException saying that the fragment hosting the AsyncTask and the Dialogfragment is no longer attached to the activity. So I tried to override both the onAttach() and the onDetach() methods (with a simple System.out.println) of my fragment, to see when the onPostExecute() gets called. The result is that when I rotate my phone, I always got this output:
onDetach
onAttach
... (if I rotate more my phone)
onPostExecute
So shouldn't the fragment be attached when the AsyncTask completes? Thank you all for your time and attention.
I've finally managed to solve this problem by stop using AsyncTask and using LoaderManager + AsyncTaskLoader following this article. In short, your fragment must implement the LoaderCallbacks interface and manage the AsyncTaskLoader. A skeleton fragment could be something like this:
public class MyFragment extends Fragment implements LoaderManager.LoaderCallbacks {
#Override
public View onCreateView(LayoutInflater inflater, final ViewGroup container,
Bundle savedInstanceState) {
// Inflate here your view as you usually do and find your components
// For example imagine to have a button tha will fire the task
Button b = (Button) view.findViewById(R.id.my_button);
b.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
// Use this to start task for the first time
getLoaderManager().initLoader(0, null, this);
// .. or this for restart the task, details in
// the provided article
// getLoaderManager().restartLoader(0, null, this);
}
});
// Get fragments load manager
LoaderManager lm = getLoaderManager();
if (lm.getLoader(0) != null) {
// Reconnect to an existing loader
lm.initLoader(0, null, this);
}
// Return your view here
return view;
}
// LoaderCallbacks methods to override
#Override
public Loader onCreateLoader(int id, Bundle args) {
// Create an instance of the loader passing the context
MyTaskLoader loader = new MyTaskLoader(getActivity());
loader.forceLoad();
return loader;
}
#Override
public void onLoadFinished(Loader loader, Object data) {
// Use this callback as you would use the AsyncTask "onPostExecute"
}
#Override
public void onLoaderReset(Loader loader) {}
// Now define the loader class
private static class MyTaskLoader extends AsyncTaskLoader {
public MyTaskLoader(Context context){
super(context);
}
#Override
public Object loadInBackground() {
// Do here your async work
}
}
}
I have a a single activity application with two fragments:
A fragment with all the UI;
A fragment without a view that has an AsyncTask as its member, and setRetainInstance set to true.
The goal is to keep the AsyncTask running even after the activity gets destroyed, and reuse it when the application comes back to focus.
I am not using setTargetFragment, all communication between fragments is done via the MainActivity.
What I thought setRetainInstance did is prevent the fragment from being recreated and keep the exact same instance at my disposal, so when I call findFragmentByTag when recreating a destroyed activity, it should return the same retained instance as when it got created, but that does not seem to be the case.
The result is that I end up with a noUi fragment that keeps counting in the background (I can see the bastard in the debugger), and another, recreated one that does not have the reference to my running AsyncTask...
What am I doing wrong?
Here's some code:
public class MainActivity extends Activity
implements FragmentCounter.Callback, FragmentMainScreen.Callback {
private static final String TAG_MAINFRAGMENT = "TAG_MAINFRAGMENT";
private static final String TAG_COUNTERFRAGMENT = "TAG_COUNTERFRAGMENT";
private FragmentMainScreen mFragmentMain;
private FragmentCounter mFragmentCounter;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (savedInstanceState == null) {
mFragmentMain = FragmentMainScreen.getInstance();
mFragmentCounter = FragmentCounter.getInstance();
getFragmentManager().beginTransaction()
.add(R.id.container, mFragmentMain, TAG_MAINFRAGMENT)
.add(mFragmentCounter, TAG_COUNTERFRAGMENT)
.commit();
} else {
mFragmentMain = (FragmentMainScreen) getFragmentManager()
.findFragmentByTag(TAG_MAINFRAGMENT);
//The fragment that gets returned here is not the same instance as the one
//I returned with getInstance() above.
mFragmentCounter = (FragmentCounter) getFragmentManager()
.findFragmentByTag(TAG_COUNTERFRAGMENT);
}
}
}
The noGui Fragment:
public class FragmentCounter extends Fragment
implements CounterAsyncTask.Callback, FragmentMainScreen.Callback {
private Callback mListener;
private CounterAsyncTask mCounterTask;
public static FragmentCounter getInstance(){
return new FragmentCounter();
}
public interface Callback {
public void onData(int aValue);
}
#Override
public void onAttach(Activity activity){
super.onAttach(activity);
if (activity instanceof Callback)
mListener = (Callback) activity;
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return null;
}
#Override
public void onValueChanged(int value) {
//In the debugger, I can see this callback beeing called,
//even after my activity gets destroyed.
//The mListener is null since I set it that way in the onDetach to
//prevent memory leaks.
//The new activity has a different instance of this Fragment.
if (mListener != null)
mListener.onData(value);
}
#Override
public void startCounting(int from) {
mCounterTask = new CounterAsyncTask(this);
mCounterTask.execute(from);
}
#Override
public void onDetach() {
super.onDetach();
mListener = null;
}
}
The AsyncTask:
public class CounterAsyncTask extends AsyncTask<Integer, Integer, Void>{
private int counter;
private Callback mListener;
private static final int SKIP = 5000;
public CounterAsyncTask(Callback listener) {
mListener = listener;
}
#Override
protected Void doInBackground(Integer...values) {
if (values != null)
counter = values[0];
while(!this.isCancelled()){
publishProgress(counter+=SKIP);
try{
Thread.sleep(SKIP);
}
catch(InterruptedException e){
e.printStackTrace();
}
}
return null;
}
#Override
protected void onProgressUpdate(Integer... values) {
mListener.onValueChanged(values[0]);
}
public interface Callback{
public void onValueChanged(int value);
}
}
Thanks in advance!
My mistake. With setRetainInstance the Fragment is retained only upon configuration change. So the fragment state will be maintained on screen rotation or if the activity is in the background but not if the activity gets destroyed.
To achieve the result I wanted I should probably use a Service.
On my main activity I have a Fragment in which I apply setRetainInstance(true) so that the AsyncTask I use into it is not disturbed by orientation change.
A lot of work is processed by the AsyncTask. That's why I would like to display a dialog with a progressBar on top of my activity.
I made some researches and I succeed in doing with a DialogFragment:
public class DialogWait extends DialogFragment {
private ProgressBar progressBar;
public DialogWait() {
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.dialog_wait, container);
Dialog dialog = getDialog();
dialog.setTitle("Hello");
setCancelable(false);
progressBar = (ProgressBar) view.findViewById(R.id.progress);
return view;
}
public void updateProgress(int value) {
progressBar.setProgress(value);
}
And here is my AsyncTask:
public class InitAsyncTask extends AsyncTask<Void, Integer, Void> {
private Context activity;
private OnTaskDoneListener mCallback;
private DialogWait dialog;
public InitAsyncTask(Context context, OnTaskDoneListener callback, DialogWait dialogWait) {
activity = context;
mCallback = callback;
dialog = dialogWait;
}
#Override
protected Void doInBackground(Void... params) {
doStuff();
return null;
}
#Override
protected void onProgressUpdate(Integer... values) {
dialog.updateProgress(values[0]);
}
#Override
protected void onPostExecute(Void result) {
publishProgress(100);
if(dialog != null)
dialog.dismiss();
mCallback.onTaskDone();
}
private void doStuff() {
//...
}
}
If I don't change the screen rotation, it works fine. But if I do, the dialog is dismissed and a few seconds later, I got a NullPointerEsception which nonsense since I set the condition: if(dialog != null)
What am I doing wrong?
Solution found!
I was not doing the right thing with the Fragment containing my AsyncTask.
Because, I haven't really understood the concept of orientation in Fragment, I get it thanks to this link: http://www.androiddesignpatterns.com/2013/04/retaining-objects-across-config-changes.html
Override onCreate, and onDestroyView methods in your DialogWait as follows:
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
#Override
public void onDestroyView() {
if (getDialog() != null && getRetainInstance()) {
getDialog().setDismissMessage(null);
}
super.onDestroyView();
}
I have created simple application with login feature. I have created separate task for do the login into server called LoginTask and a listener class called LoginListener.
public interface LoginListener {
public void onLoginComplete();
public void onLoginFailure(String msg);
}
public class LoginTask extends AsyncTask<String, Void, Boolean>{
private final LoginListener listener;
private final Context c;
private String msg;
public LoginTask(final Context c, final LoginListener listener) {
this.c = c;
this.listener = listener;
}
#Override
protected Boolean doInBackground(String... args) {
// loging in to server
//return true if success
}
#Override
protected void onPostExecute(Boolean status) {
if(!status){
if(listener != null) listener.onLoginFailure(msg);
return;
}
// the problem is here, listener is null, because activity/fragment destroyed
if(listener != null) listener.onLoginComplete();
}
}
I executed LoginTask from LoginFragment. The LoginFragment implements LoginListener.
public class LoginFragment extends Fragment implements LoginListener{
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.frg_login, container, false);
}
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
doInitView();
};
private void doInitView(){
Button loginButton = (Button) getActivity().findViewById(R.id.login_btn);
Button regButton = (Button) getActivity().findViewById(R.id.toreg_btn);
ButtonListener listener = new ButtonListener();
loginButton.setOnClickListener(listener);
regButton.setOnClickListener(listener);
}
private void doLogin(){
Activity activity = getActivity();
EditText emailText = (EditText)activity.findViewById(R.id.login_email);
EditText pwdText = (EditText)activity.findViewById(R.id.login_pwd);
String email = emailText.getText().toString().trim();
String pwd = pwdText.getText().toString().trim();
if(StringUtil.isAnyNull(email, pwd)){
Popup.showMsg(getActivity(), "Silahkan lengkapi data", Popup.SHORT);
return;
}
savedEmail = email;
savedPwd = pwd;
String url = getActivity().getResources().getString(R.string.url_login);
Popup.showLoading(getActivity(), "Login", "Please wait...");
LoginTask task = new LoginTask(getActivity(), this);
task.execute(url, email, pwd);
}
private final class ButtonListener implements OnClickListener{
#Override
public void onClick(View v) {
switch(v.getId()){
case R.id.login_btn:
doLogin();
break;
case R.id.toreg_btn:
doToRegister();
break;
case R.id.demo_btn:
doDemo();
break;
}
}
}
#Override
public void onLoginComplete() {
// getActivity() is null
((MainActivity)getActivity()).gotoMain();
}
#Override
public void onLoginFailure(String msg) {
}
}
Because of the login task takes time, sometime the device light turn off before the task was finished so activity was destroyed. This caused the task failed to call the listener(fragment). How to solve this problem?
Thanks
AsyncTask should be used for tasks that take a bit longer and return a result into the current activity. However it's not intended for really long running tasks or for those cases where you want to evaluate its results even if the activity has been destroyed. You might consider using a Service here. In any case you shouldn't do updates in onPostExecute() anymore cause the activity context might be gone (see Doctoror Drive's post). Having that service in place, you can either send an Intent or a Broadcast event to the system. Then do the further processing in that intent activity / broadcast receiver.
You can cancel the asynctask in onDestroy() of your LoginActivity.
Override onCancelled() of the asynctask. When the activity is destroyed, a call to onCancelled() will be made instead of onPostExecute()
Here you can avoid a call back to the LoginActivity.
You should use Service or IntentService. because AsyncTask does not record any variables or context of Activity. When you finish login task launch PendingIntent or startActivity(intent). This can be best practice of Android. This way you never get exception.
In onLoginComplete and onLoginFailure check if the fragment is still attached to the activity. If not, do nothing.
#Override
public void onLoginComplete() {
if (isAdded() && !isRemoving() && !isDetached()) {
((MainActivity)getActivity()).gotoMain();
}
}
I have spent many hours looking for a solution to this and need help.
I have a nested AsyncTask in my Android app Activity and I would like to allow the user to rotate his phone during it's processing without starting a new AsyncTask. I tried to use onRetainNonConfigurationInstance() and getLastNonConfigurationInstance().
I am able to retain the task; however after rotation it does not save the result from onPostExecute() to the outer class variable. Of course, I tried getters and setters. When I dump the variable in onPostExecute, that it is OK. But when I try to access to the variable from onClick listener then it is null.
Maybe the code will make the problem clear for you.
public class MainActivity extends BaseActivity {
private String possibleResults = null;
private Object task = null;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.task = getLastNonConfigurationInstance();
setContentView(R.layout.menu);
if ((savedInstanceState != null)
&& (savedInstanceState.containsKey("possibleResults"))) {
this.possibleResults = savedInstanceState
.getString("possibleResults");
}
if (this.possibleResults == null) {
if (this.task != null) {
if (this.task instanceof PossibleResultWebService) {
((PossibleResultWebService) this.task).attach();
}
} else {
this.task = new PossibleResultWebService();
((PossibleResultWebService) this.task).execute(this.matchToken);
}
}
Button button;
button = (Button) findViewById(R.id.menu_resultButton);
button.setOnClickListener(resultListener);
}
#Override
protected void onResume() {
super.onResume();
}
OnClickListener resultListener = new OnClickListener() {
#Override
public void onClick(View v) {
Spinner s = (Spinner) findViewById(R.id.menu_heatSpinner);
int heatNo = s.getSelectedItemPosition() + 1;
Intent myIntent = new Intent(MainActivity.this,
ResultActivity.class);
myIntent.putExtra("matchToken", MainActivity.this.matchToken);
myIntent.putExtra("heatNo", String.valueOf(heatNo));
myIntent.putExtra("possibleResults",
MainActivity.this.possibleResults);
MainActivity.this.startActivityForResult(myIntent, ADD_RESULT);
}
};
private class PossibleResultWebService extends AsyncTask<String, Integer, Integer> {
private ProgressDialog pd;
private InputStream is;
private boolean finished = false;
private String possibleResults = null;
public boolean isFinished() {
return finished;
}
public String getPossibleResults() {
return possibleResults;
}
#Override
protected Integer doInBackground(String... params) {
// quite long code
}
public void attach() {
if (this.finished == false) {
pd = ProgressDialog.show(MainActivity.this, "Please wait...",
"Loading data...", true, false);
}
}
public void detach() {
pd.dismiss();
}
#Override
protected void onPreExecute() {
pd = ProgressDialog.show(MainActivity.this, "Please wait...",
"Loading data...", true, false);
}
#Override
protected void onPostExecute(Integer result) {
possibleResults = convertStreamToString(is);
MainActivity.this.possibleResults = possibleResults;
pd.dismiss();
this.finished = true;
}
}
#Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (this.possibleResults != null) {
outState.putString("possibleResults", this.possibleResults);
}
}
#Override
public Object onRetainNonConfigurationInstance() {
if (this.task instanceof PossibleResultWebService) {
((PossibleResultWebService) this.task).detach();
}
return (this.task);
}
}
It is because you are creating the OnClickListener each time you instantiate the Activity (so each time you are getting a fresh, new, OuterClass.this reference), however you are saving the AsyncTask between Activity instantiations and keeping a reference to the first instantiated Activity in it by referencing OuterClass.this.
For an example of how to do this right, please see https://github.com/commonsguy/cw-android/tree/master/Rotation/RotationAsync/
You will see he has an attach() and detach() method in his RotationAwareTask to solve this problem.
To confirm that the OuterClass.this reference inside the AsyncTask will always point to the first instantiated Activity if you keep it between screen orientation changes (using onRetainNonConfigurationInstance) then you can use a static counter that gets incremented each time by the default constructor and keep an instance level variable that gets set to the count on each creation, then print that.