Unexpected lifecycle causing Fatal Exception? - android

I'm an iOS dev, with absolutely no experience with android, so forgive me if I have difficulty expressing my question:
I'm getting a Fatal Exception in an android app I'm tasked with maintaining.
Fatal Exception: java.lang.RuntimeException Unable to start activity ComponentInfo{…}: java.lang.NullPointerException
I can see from the crash report (attached below) that the crash is happening in one of my fragments in a method called "bindLocation"
Here is the line it's crashing on:
mLocationHeaderTextView.setVisibility(View.VISIBLE);
So clearly the problem is that mLocationHeaderTextview is null. mLocationHeaderTextView is injected using roboguice as follows:
#InjectView(R.id.filter_location_header)
TextView mLocationHeaderTextView;
Now, I think the error is caused by my textView not being 'injected'. After looking at the stack trace, I've determine that this is likely a result of onViewCreated() not getting called before bindLocation() is called.
This is where I'm getting lost, however. I'm very unfamiliar with android, so I'm not sure how it's possible that my onViewCreated method is getting skipped over. I've tried a million things on the device to try and reproduce this situation, but I am unable. No matter what user actions I can think to try, my onViewCreated method gets called and my variable is non-Null. Perhaps I'm misreading the stack trace. I'm hoping that my stack trace below provides enough information for y'all to help me, but if not, let me know what else I can tell you about the app. I cannot post my full source code (as it does not belong to me), but I will provide whatever information I can.
Thanks so much for your help!
From my FilterDrawerFragment:
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_filter_drawer, container, false);
ButterKnife.inject(this, view);
return view;
}
#Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mRulesHeaderReset.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
resetRules();
}
});
if (getActivity() != null && mDrawerLayout == null) {
mDrawerLayout = (DrawerLayout) getActivity().findViewById(R.id.filter_drawer_layout);
if (mDrawerLayout != null) {
mDrawerLayout.setDrawerListener(new DrawerLayout.DrawerListener() {
#Override
public void onDrawerSlide(View view, float v) {
}
#Override
public void onDrawerOpened(View view) {
NavigationUtils.invalidateOptionsMenu(getActivity());
}
#Override
public void onDrawerClosed(View view) {
notifyFiltersChanged();
NavigationUtils.invalidateOptionsMenu(getActivity());
}
#Override
public void onDrawerStateChanged(int i) {
}
});
// set a custom shadow that overlays the main content when the drawer opens
mDrawerLayout.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.END);
}
}
mlocationChangeButton.setOnClickListener(this);
bindUI();
}
private void bindUI() {
if (isAdded() && mConfiguration != null) {
// bind the location
bindLocation();
…
}
}
private void bindLocation() {
if (isAdded() && mConfiguration != null && mConfiguration.isLocationEnabled()) {
// show the location related items
mLocationHeaderTextView.setVisibility(View.VISIBLE);//Crash reported here.
…
}
});
}
}
/**
* Users of this fragment must call this to update the filter configuration
*/
public void updateConfiguration(FilterConfiguration configuration) {
if (configuration != null && !configuration.equals(mConfiguration)) {
mConfiguration = configuration;
bindUI();
}
}
And updateConfiguration() is called from
LocationFinderFragment:
#Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (getArguments() != null) {
………
if (savedInstanceState == null) {
mFilterDrawerFragment = FilterDrawerFragment.newInstance();
getChildFragmentManager()
.beginTransaction()
.replace(R.id.filter_drawer, mFilterDrawerFragment)
.commit();
} else {
mFilterDrawerFragment = (FilterDrawerFragment) getChildFragmentManager().findFragmentById(R.id.filter_drawer);
}
………
);
…………………
populateFilters();
}
private void populateFilters() {
// these might be passed into the fragment
if (mFacets != null) {
FilterConfiguration config = new FilterConfiguration() {{
……………………
}};
mFilterDrawerFragment.updateConfiguration(config);

Now I think I see the problem.
In roboguice views are injected in onViewCreated().
onViewCreated() is called after onCreateView().
bindLocation() is called in onCreateView().
Hence, by this time views are not yet injected by roboguice.
Please try calling bindLocation() in onViewCreated() after call to super.

This is how I fixed this. As you are using ButterKnife. Without these Configuration ButterKnife throws NPE.
Before you use this library you have to do some configuration to ButterKnife Eclipse Configuration or
ButterKnife Android Studio Configuration
If you are using ADT and missing Annototion Processing Check out this to install Android Eclipse - Cannot see annotation processing option

Related

Why I'm getting java.lang.IllegalArgumentException during android.support.v4.app.Fragment.setTargetFragment

After upgrading my app to
targetSdkVersion 27
com.android.support:support-v4:27.0.2
I am getting the following crash log from live
Caused by: java.lang.IllegalArgumentException:
at android.support.v4.app.Fragment.setTargetFragment (Fragment.java:545)
I look at the source code of support fragment (Not exactly line 545. I'm not sure how I can get the latest source code of support fragment library)
https://android.googlesource.com/platform/frameworks/support/+/cef09fe/v4/java/android/support/v4/app/Fragment.java#598
598 public void setTargetFragment(Fragment fragment, int requestCode) {
599 mTarget = fragment;
560 mTargetRequestCode = requestCode;
561 }
I can't see a reason why java.lang.IllegalArgumentException is thrown. Maybe I'm looking at wrong version of support library source code?
Do you have any idea, why I'm getting java.lang.IllegalArgumentException during android.support.v4.app.Fragment.setTargetFragment
My source code is as follow
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
public class BuyPortfolioFragment extends Fragment {
...
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
// If not retained (or first time running), we need to create it.
if (this.statusBarUpdaterFragment == null) {
this.statusBarUpdaterFragment = StatusBarUpdaterFragment.newInstance();
// Tell it who it is working with.
this.statusBarUpdaterFragment.setTargetFragment(this, 0);
fm.beginTransaction().add(statusBarUpdaterFragment, STATUS_BAR_UPDATER_FRAGMENT).commitAllowingStateLoss();
} else {
statusBarUpdaterFragment.setTargetFragment(this, 0);
}
This is my code, which may crash sometimes. You may reproduce, by reducing background process limit to 1.
public class BuyPortfolioFragment extends Fragment {
...
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
// If not retained (or first time running), we need to create it.
if (this.statusBarUpdaterFragment == null) {
this.statusBarUpdaterFragment = StatusBarUpdaterFragment.newInstance();
// Tell it who it is working with.
this.statusBarUpdaterFragment.setTargetFragment(this, 0);
fm.beginTransaction().add(statusBarUpdaterFragment, STATUS_BAR_UPDATER_FRAGMENT).commitAllowingStateLoss();
} else {
statusBarUpdaterFragment.setTargetFragment(this, 0);
}
Note, calling setTargetFragment might not be always necessary due to Is calling setTargetFragment on headless fragment no longer necessary during UI fragment re-creation?
However, to play a safe game, I use the following fix.
public class BuyPortfolioFragment extends Fragment {
...
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
// If not retained (or first time running), we need to create it.
if (this.statusBarUpdaterFragment == null) {
this.statusBarUpdaterFragment = StatusBarUpdaterFragment.newInstance();
// Tell it who it is working with.
this.statusBarUpdaterFragment.setTargetFragment(this, 0);
fm.beginTransaction().add(statusBarUpdaterFragment, STATUS_BAR_UPDATER_FRAGMENT).commitAllowingStateLoss();
} else {
// Google poses new restriction recently. A fragment need to have same fragment
// manager as StatusBarUpdaterFragment, in order to become its target fragment. It
// is OK not to call setTargetFragment. Although you may get null from
// statusBarUpdaterFragment.getTargetFragment, during onResume,
// statusBarUpdaterFragment.getTargetFragment will magically return correct fragment.
//statusBarUpdaterFragment.setTargetFragment(this, 0);
}
}
#Override
public void onResume() {
super.onResume();
...
// This code block might not be necessary. Just to be safe.
if (statusBarUpdaterFragment != null) {
// Google poses new restriction recently. A fragment need to have same fragment
// manager as StatusBarUpdaterFragment, in order to become its target fragment. It
// is OK not to call setTargetFragment. Although you may get null from
// statusBarUpdaterFragment.getTargetFragment, during onResume,
// statusBarUpdaterFragment.getTargetFragment will magically return correct fragment.
try {
statusBarUpdaterFragment.setTargetFragment(this, 0);
} catch (java.lang.IllegalArgumentException e) {
// Shouldn't happen. Just to be safe.
Log.e(TAG, "", e);
}
}

AndroidRuntimeException:requestFeature() must be called before adding content in DialogFragment

I understand this question have been asking many times here but after spending the last 1,5 hour reading and trying to sort out my issue, I can't.
Problem statement:
When calling setStyle method in DialogFragment i get the RuntimeException error stated in the title.
This is my original code, which does not throws this exception:
public class MapDialogFragment extends DialogFragment
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setStyle(DialogFragment.STYLE_NO_FRAME, android.R.style.Theme_Holo);
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
view = inflater.inflate(R.layout.maps_dialog, container, false);
return view;
}
}
Now, I have just added ImmersiveMode throughout my application. As some of you may know, the immersive mode is lost when showing Dialogs, so one must override these fragments and set appropriate flags so that the mode is kept. I have successfully accomplished this - it works. But, I have to comment out line: setStyle(DialogFragment.STYLE_NO_FRAME, android.R.style.Theme_Holo);, so I am losing my dialog's style and this is the problem.
Having a closer look at the setStyle method in the DialogFragment, I can't really see how requestFeature() is being invoked:
public void setStyle(int style, int theme) {
mStyle = style;
if (mStyle == STYLE_NO_FRAME || mStyle == STYLE_NO_INPUT) {
mTheme = com.android.internal.R.style.Theme_DeviceDefault_Dialog_NoFrame;
}
if (theme != 0) {
mTheme = theme;
}
}
Finally, this is my DialogFragment class where the exception is happening. Note the getWindow().setFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE); and also the clearFlag, which were necessary to have the ImmersiveMode working (hopefully this will be useful for someone):
public class MapDialogFragmentv2 extends DialogFragment {
#Override
public void onCreate(Bundle savedInstanceState) {
//I have also tried the code here, before the super.OnCreate but to no avail
//setStyle(DialogFragment.STYLE_NO_FRAME, android.R.style.Theme_Holo);
super.onCreate(savedInstanceState);
}
#Override
public Dialog onCreateDialog(final Bundle savedInstanceState) {
MyDialog mDialog = new MyDialog(getActivity());
mDialog.getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
view = getActivity().getLayoutInflater().inflate(R.layout.maps_dialog, null);
//Line below throws exception. Needs to be commented out
setStyle(DialogFragment.STYLE_NO_FRAME, android.R.style.Theme_Holo);
mDialog.setContentView(view);
return mDialog;
}
public class MyDialog extends Dialog {
public MyDialog(Context context) {
super(context);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
getWindow().getDecorView().setSystemUiVisibility(MainActivity.getImmersiveModeFlags());
}
#Override
public void show() {
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
super.show();
}
}
}
I have also tried using a mix of onCreate, onCreateView and onCreateDialog but it didn't work. I also read here at Stackoverflow someone commenting that it was not a good idea to have onCreateView and onCreateDialog at the same time.
And also tried adding the style to my xml layout itself, but also didn't work:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/lib/com.google.android.gms.plus"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="#android:color/transparent"
style="#android:style/Theme.Holo.Dialog" >
Thank you
The problem is that you mash together too many different things and that you don't respect the lifecycle of each class.
Firstly you need to stop nesting classes like you do. This is partly the source of the error. If you really want/have to nest a Fragment or Dialog then it is important that you declare the nested Fragments, Dialogs as static. If you don't declare them as static you can cause memory leaks and problems like yours. But the best thing you can do is to only limit yourself to one class per file unless you have an actual reason to nest it. This also has the added benefit of improving readability and maintainability of your code.
But the main cause of your error is that you completely ignore the lifecycle of the Dialog. Pretty much all of the following code should be in the proper lifecycle methods of the Dialog:
mDialog.getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
view = getActivity().getLayoutInflater().inflate(R.layout.maps_dialog, null);
//Line below throws exception. Needs to be commented out
setStyle(DialogFragment.STYLE_NO_FRAME, android.R.style.Theme_Holo);
mDialog.setContentView(view);
So to fix your error you need to do 3 things:
Either declare MapDialogFragmentv2 and MyDialog static like this:
public class MainActivity extends Activity {
...
public static class MapDialogFragmentv2 extends Fragment {
...
public static class MyDialog extends Dialog {
...
}
}
}
Or even better move them to their own files all together.
Move the code from onCreateDialog() in MapDialogFragmentv2 in the correct lifecycle methods in MyDialog. It should then look something like this:
public static class MyDialog extends Dialog {
private final LayoutInflater mInflater;
public MyDialog(Context context) {
super(context);
mInflater = LayoutInflater.from(context);
}
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
getWindow().getDecorView().setSystemUiVisibility(MainActivity.getImmersiveModeFlags());
View view = mInflater.inflate(R.layout.maps_dialog, null);
setContentView(view);
}
#Override
public void show() {
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
super.show();
}
}
Add setStyle(DialogFragment.STYLE_NO_FRAME, android.R.style.Theme_Holo); to the onCreate() method of your MapDialogFragmentv2 after the super.onCreate() call.
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setStyle(DialogFragment.STYLE_NO_FRAME, android.R.style.Theme_Holo);
}
Your MapDialogFragmentv2 should then just look like this:
public static class MapDialogFragmentv2 extends DialogFragment {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setStyle(DialogFragment.STYLE_NO_FRAME, android.R.style.Theme_Holo);
}
#Override
public Dialog onCreateDialog(final Bundle savedInstanceState) {
return new MyDialog(getActivity());
}
}
I tested everything on my Nexus 5 running Android 5.0.1 (Lollipop) and it works.
I was getting this exception on API 21 and API 23.
Fatal Exception: android.util.AndroidRuntimeException: requestFeature() must be called before adding content
at com.android.internal.policy.impl.PhoneWindow.requestFeature + 301(PhoneWindow.java:301)
at android.app.Dialog.requestWindowFeature + 1060(Dialog.java:1060)
at androidx.fragment.app.DialogFragment.setupDialog + 403(DialogFragment.java:403)
at androidx.fragment.app.DialogFragment.onGetLayoutInflater + 383(DialogFragment.java:383)
at androidx.fragment.app.Fragment.performGetLayoutInflater + 1403(Fragment.java:1403)
at androidx.fragment.app.FragmentManagerImpl.moveToState + 880(FragmentManagerImpl.java:880)
at androidx.fragment.app.FragmentManagerImpl.moveFragmentToExpectedState + 1237(FragmentManagerImpl.java:1237)
at androidx.fragment.app.FragmentManagerImpl.moveToState + 1302(FragmentManagerImpl.java:1302)
at androidx.fragment.app.BackStackRecord.executeOps + 439(BackStackRecord.java:439)
at androidx.fragment.app.FragmentManagerImpl.executeOps + 2075(FragmentManagerImpl.java:2075)
at androidx.fragment.app.FragmentManagerImpl.executeOpsTogether + 1865(FragmentManagerImpl.java:1865)
at androidx.fragment.app.FragmentManagerImpl.removeRedundantOperationsAndExecute + 1820(FragmentManagerImpl.java:1820)
at androidx.fragment.app.FragmentManagerImpl.execPendingActions + 1726(FragmentManagerImpl.java:1726)
at androidx.fragment.app.FragmentManagerImpl$2.run + 150(FragmentManagerImpl.java:150)
at android.os.Handler.handleCallback + 739(Handler.java:739)
at android.os.Handler.dispatchMessage + 95(Handler.java:95)
at android.os.Looper.loop + 135(Looper.java:135)
at android.app.ActivityThread.main + 5221(ActivityThread.java:5221)
at java.lang.reflect.Method.invoke(Method.java)
at java.lang.reflect.Method.invoke + 372(Method.java:372)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run + 899(ZygoteInit.java:899)
at com.android.internal.os.ZygoteInit.main + 694(ZygoteInit.java:694)
This was the offending code:
public Dialog onCreateDialog(Bundle savedInstanceState) {
Dialog dialog = super.onCreateDialog(savedInstanceState);
View decorView = dialog.getWindow().getDecorView();
// do some stuff to decorView
return dialog;
}
TLDR
I had to change it to:
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
View decorView = dialog.getWindow().getDecorView();
// do some stuff to decorView
}
Slightly Deeper Analysis without References
Technically, it's possible to perform my Window#getDecorView call (which triggers PhoneWindow#installDecor) any time after DialogFragment#setupDialog, and the earliest place to do that seems to be within onCreateView (before or after super.onCreateView doesn't matter). However, I prefer to let DialogFragment cause PhoneWindow#installDecor with DialogFragment#onActivityCreated, which seems like the normal path. I can then call Window#getDecorView within my own onActivityCreated AFTER calling super.onActivityCreated.
Super Deep Analysis with Android Source References
The problem was that Window#getDecorView creates window decorations which makes requesting window features throw the AndroidRuntimeException. See the com.android.impl.policy.PhoneWindow#getDecorView source:
public final View getDecorView() {
if (mDecor == null) {
installDecor();
}
return mDecor;
}
Notice that com.android.impl.policy.PhoneWindow#installDecor sets mContentParent:
private void installDecor() {
if (mDecor == null) {
mDecor = generateDecor();
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
// ^ this is our culprit
// more window stuff that isn't relevant
}
And that requestFeature throws an AndroidRuntimeException if mContentParent is set:
public boolean requestFeature(int featureId) {
if (mContentParent != null) {
throw new AndroidRuntimeException("requestFeature() must be called before adding content");
}
// more requestFeature stuff that isn't relevant
}
And of course DialogFragment#setupDialog calls Window#requestFeature, which is the where our stack trace comes from:
public void setupDialog(#NonNull Dialog dialog, int style) {
switch (style) {
case STYLE_NO_INPUT:
dialog.getWindow().addFlags(
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE);
// fall through...
case STYLE_NO_FRAME:
case STYLE_NO_TITLE:
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
}
}
And one level up in the stack trace, we see that DialogFragment#onGetLayoutInflator calls setupDialog, but it calls onCreateDialog before that:
public LayoutInflater onGetLayoutInflater(#Nullable Bundle savedInstanceState) {
if (!mShowsDialog) {
return super.onGetLayoutInflater(savedInstanceState);
}
mDialog = onCreateDialog(savedInstanceState);
if (mDialog != null) {
setupDialog(mDialog, mStyle);
return (LayoutInflater) mDialog.getContext().getSystemService(
Context.LAYOUT_INFLATER_SERVICE);
}
return (LayoutInflater) mHost.getContext().getSystemService(
Context.LAYOUT_INFLATER_SERVICE);
}
And FragmentManagerImpl calls Fragment#performGetLayoutInflater (which calls Fragment#onGetLayoutInflator) right before it calls Fragment#performCreateView (which calls Fragment#onCreateView), then Fragment#onViewCreated, then Fragment#performActivityCreated (which calls Fragment#onActivityCreated):
Fragment f; // fragment
if (DEBUG) Log.v(TAG, "moveto ACTIVITY_CREATED: " + f);
if (!f.mFromLayout) {
ViewGroup container = null;
if (f.mContainerId != 0) {
if (f.mContainerId == View.NO_ID) {
throwException(new IllegalArgumentException(
"Cannot create fragment "
+ f
+ " for a container view with no id"));
}
container = (ViewGroup) mContainer.onFindViewById(f.mContainerId);
if (container == null && !f.mRestored) {
String resName;
try {
resName = f.getResources().getResourceName(f.mContainerId);
} catch (Resources.NotFoundException e) {
resName = "unknown";
}
throwException(new IllegalArgumentException(
"No view found for id 0x"
+ Integer.toHexString(f.mContainerId) + " ("
+ resName
+ ") for fragment " + f));
}
}
f.mContainer = container;
f.performCreateView(f.performGetLayoutInflater(
f.mSavedFragmentState), container, f.mSavedFragmentState);
if (f.mView != null) {
f.mView.setSaveFromParentEnabled(false);
setViewTag(f);
if (container != null) {
container.addView(f.mView);
}
if (f.mHidden) {
f.mView.setVisibility(View.GONE);
}
ViewCompat.requestApplyInsets(f.mView);
f.onViewCreated(f.mView, f.mSavedFragmentState);
mLifecycleCallbacksDispatcher.dispatchOnFragmentViewCreated(
f, f.mView, f.mSavedFragmentState, false);
// Only animate the view if it is visible. This is done after
// dispatchOnFragmentViewCreated in case visibility is changed
f.mIsNewlyAdded = (f.mView.getVisibility() == View.VISIBLE)
&& f.mContainer != null;
}
}
f.performActivityCreated(f.mSavedFragmentState);
And DialogFragment calls Dialog#setContentView in onActivityCreated:
public void onActivityCreated(#Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (!mShowsDialog) {
return;
}
View view = getView();
if (view != null) {
if (view.getParent() != null) {
throw new IllegalStateException(
"DialogFragment can not be attached to a container view");
}
mDialog.setContentView(view);
}
// more irrelevant onActivityCreated stuff
}
Thus, we shouldn't do anything to trigger PhoneWindow#installDecor before DialogFragment#onActivityCreated so that DialogFragment and PhoneWindow can play together as they expect.
Then, according to onCreateDialog's JavaDoc:
This method will be called after onCreate(android.os.Bundle) and before Fragment.onCreateView(android.view.LayoutInflater, android.view.ViewGroup, android.os.Bundle)
And Window#requestFeature's JavaDoc confirms what our exception said
This must be called before setContentView()
Thus, my Window#getDecorView caused the crash when I called it any time within onCreateDialog because Window#getDecorView triggers PhoneWindow#installDecor, which sets PhoneWindow#mContentParent, which triggers the AndroidRuntimeException when DialogFragment#onGetLayoutInflator calls setupDialog right after onCreateDialog and setupDialog calls Window#requestFeature.
Technically, it's possible to perform my Window#getDecorView call (which triggers PhoneWindow#installDecor) any time after DialogFragment#setupDialog, and the earliest place to do that seems to be within onCreateView (before or after super.onCreateView doesn't matter). However, I prefer to let DialogFragment cause PhoneWindow#installDecor with DialogFragment#onActivityCreated, which seems like the normal path. I can then call Window#getDecorView within my own onActivityCreated AFTER calling super.onActivityCreated.
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
View decorView = dialog.getWindow().getDecorView();
// do some stuff to decorView
}
on some android6.0 phones, I found that I had to request feature before super.onCreate(savedInstanceState)
I solved the issue of not having a Fullscreen Dialog in the MapDialogFragmentv2:
#Override
public void onStart() {
super.onStart();
Dialog dialog = getDialog();
if (dialog != null) {
int width = ViewGroup.LayoutParams.MATCH_PARENT;
int height = ViewGroup.LayoutParams.MATCH_PARENT;
dialog.getWindow().setLayout(width, height);
}
}
Now the only thing missing is that the setStyle() method is not working. The style is not being set. But it does not throw the original exception mentioned in this post.
Ok,
it's almost working now, so I will mark Xaver answer's as correct as it lead me to the right way.
I had to change the MyDialog class to:
public MyDialog(Context context, int theme) {
super(context,theme);
mInflater = LayoutInflater.from(context);
}
That way I manage to keep the Theme. But for some reason my buttons are not following the Theme. They are all back to default. I am trying to programatically set the style, but with no success for now
buttonGuess.setBackgroundColor(android.R.attr.borderlessButtonStyle);
buttonOk.setBackgroundColor(android.R.style.Widget_Holo_Button);
Nonetheless, it now works as I want it.
Thanks everyone

getActivity returns null in Fragment

I have a fragment which is basically a list view. The parent activity calls a method to retrieve a list of roster items from a service. When the data returns from the service I call updateRosterItems on the fragment passing through and ArrayList of Roster items. The problem is that it works the first time through, but then when I select a different tab, and then come back to the tab with the fragment, the getActivity() returns null and I can't hook up the data to the ArrayAdapter.
This is the code for the updateRosterItems function:
public void updateRosterList(ArrayList<RosterInfo> rosterItems)
{
if(_adapter == null)
{
_adapter = new RosterItemAdapter(getActivity(), R.layout.roster_listview_item, rosterItems);
}
Activity activity = getActivity();
if(activity != null)
{
ListView list = (ListView)activity.findViewById(R.id.lstRosterItems);
list.setAdapter(_adapter);
_adapter.notifyDataSetChanged();
}
}
I've read about similar issues caused by code being called before the fragment is attached. I guess my question is, is there a way to delay the call to the updateRosterList until after the onAttach is called? The solution I'm toying with is that if getActivity() returns null then store the data in private variable in the fragment, and in the onAttach method check if there is data in the varialbe and then call the update on the adapter. This seems a bit hacky though. Any ideas?
UPDATE: I've managed to get it working by doing this. I'm quite new to Android development and it seems a bit hacky to me as a solution. Is there a better way? Basically the updateRosterList function is the one that is called from outside of the fragment.
public class RosterListFragment extends Fragment {
RosterItemAdapter _adapter = null;
private ArrayList<RosterInfo> _items;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
return inflater.inflate(R.layout.roster_listview, container, false);
}
#Override
public void onActivityCreated(Bundle savedInstanceState)
{
super.onActivityCreated(savedInstanceState);
if(_items != null)
{
performUpdateRosterList(_items);
}
}
public void updateRosterList(ArrayList<RosterInfo> rosterItems)
{
Activity activity = getActivity();
if(activity != null)
{
performUpdateRosterList(rosterItems);
}
else
{
_items = rosterItems;
}
}
private void performUpdateRosterList(ArrayList<RosterInfo> rosterItems)
{
Activity activity = getActivity();
if(_adapter == null)
{
_adapter = new RosterItemAdapter(activity, R.layout.roster_listview_item, rosterItems);
}
ListView list = (ListView)activity.findViewById(R.id.lstRosterItems);
list.setAdapter(_adapter);
_adapter.notifyDataSetChanged();
}
}
You are correct, the activity isn't yet attached. There's two ways to handle this.
Don't make the changes until after the activity has been attached. Perhaps just save off rosterItems, and have it updated later.
Pass in the context into your updater function.
Personally, I would say the first is probably be better path, but either one could work fine.

Android Fragments. Retaining an AsyncTask during screen rotation or configuration change

I'm working on a Smartphone / Tablet app, using only one APK, and loading resources as is needed depending on screen size, the best design choice seemed to be using Fragments via the ACL.
This app has been working fine until now being only activity based. This is a mock class of how I handle AsyncTasks and ProgressDialogs in the Activities in order to have them work even when the screen is rotated or a configuration change occurs mid communication.
I will not change the manifest to avoid recreation of the Activity, there are many reasons why I dont want to do it, but mainly because the official docs say it isnt recomended and I've managed without it this far, so please dont recomend that route.
public class Login extends Activity {
static ProgressDialog pd;
AsyncTask<String, Void, Boolean> asyncLoginThread;
#Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
setContentView(R.layout.login);
//SETUP UI OBJECTS
restoreAsyncTask();
}
#Override
public Object onRetainNonConfigurationInstance() {
if (pd != null) pd.dismiss();
if (asyncLoginThread != null) return (asyncLoginThread);
return super.onRetainNonConfigurationInstance();
}
private void restoreAsyncTask();() {
pd = new ProgressDialog(Login.this);
if (getLastNonConfigurationInstance() != null) {
asyncLoginThread = (AsyncTask<String, Void, Boolean>) getLastNonConfigurationInstance();
if (asyncLoginThread != null) {
if (!(asyncLoginThread.getStatus()
.equals(AsyncTask.Status.FINISHED))) {
showProgressDialog();
}
}
}
}
public class LoginThread extends AsyncTask<String, Void, Boolean> {
#Override
protected Boolean doInBackground(String... args) {
try {
//Connect to WS, recieve a JSON/XML Response
//Place it somewhere I can use it.
} catch (Exception e) {
return true;
}
return true;
}
protected void onPostExecute(Boolean result) {
if (result) {
pd.dismiss();
//Handle the response. Either deny entry or launch new Login Succesful Activity
}
}
}
}
This code is working fine, I have around 10.000 users without complaint, so it seemed logical to just copy this logic into the new Fragment Based Design, but, of course, it isnt working.
Here is the LoginFragment:
public class LoginFragment extends Fragment {
FragmentActivity parentActivity;
static ProgressDialog pd;
AsyncTask<String, Void, Boolean> asyncLoginThread;
public interface OnLoginSuccessfulListener {
public void onLoginSuccessful(GlobalContainer globalContainer);
}
public void onSaveInstanceState(Bundle outState){
super.onSaveInstanceState(outState);
//Save some stuff for the UI State
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//setRetainInstance(true);
//If I setRetainInstance(true), savedInstanceState is always null. Besides that, when loading UI State, a NPE is thrown when looking for UI Objects.
parentActivity = getActivity();
}
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
loginSuccessfulListener = (OnLoginSuccessfulListener) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString() + " must implement OnLoginSuccessfulListener");
}
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
RelativeLayout loginLayout = (RelativeLayout) inflater.inflate(R.layout.login, container, false);
return loginLayout;
}
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
//SETUP UI OBJECTS
if(savedInstanceState != null){
//Reload UI state. Im doing this properly, keeping the content of the UI objects, not the object it self to avoid memory leaks.
}
}
public class LoginThread extends AsyncTask<String, Void, Boolean> {
#Override
protected Boolean doInBackground(String... args) {
try {
//Connect to WS, recieve a JSON/XML Response
//Place it somewhere I can use it.
} catch (Exception e) {
return true;
}
return true;
}
protected void onPostExecute(Boolean result) {
if (result) {
pd.dismiss();
//Handle the response. Either deny entry or launch new Login Succesful Activity
}
}
}
}
}
I cant use onRetainNonConfigurationInstance() since it has to be called from the Activity and not the Fragment, same goes with getLastNonConfigurationInstance(). I've read some similar questions here with no answer.
I understand that it might require some working around to get this stuff organized properly in fragments, that being said, I would like to maintain the same basic design logic.
What would be the proper way to retain the AsyncTask during a configuration change, and if its still runing, show a progressDialog, taking into consideration that the AsyncTask is a inner class to the Fragment and it is the Fragment itself who invokes the AsyncTask.execute()?
Fragments can actually make this a lot easier. Just use the method Fragment.setRetainInstance(boolean) to have your fragment instance retained across configuration changes. Note that this is the recommended replacement for Activity.onRetainnonConfigurationInstance() in the docs.
If for some reason you really don't want to use a retained fragment, there are other approaches you can take. Note that each fragment has a unique identifier returned by Fragment.getId(). You can also find out if a fragment is being torn down for a config change through Fragment.getActivity().isChangingConfigurations(). So, at the point where you would decide to stop your AsyncTask (in onStop() or onDestroy() most likely), you could for example check if the configuration is changing and if so stick it in a static SparseArray under the fragment's identifier, and then in your onCreate() or onStart() look to see if you have an AsyncTask in the sparse array available.
I think you will enjoy my extremely comprehensive and working example detailed below.
Rotation works, and the dialog survives.
You can cancel the task and dialog by pressing the back button (if you want this behaviour).
It uses fragments.
The layout of the fragment underneath the activity changes properly when the device rotates.
There is a complete source code download and a precompiled APK so you can see if the behaviour is what you want.
Edit
As requested by Brad Larson I have reproduced most of the linked solution below. Also since I posted it I have been pointed to AsyncTaskLoader. I'm not sure it is totally applicable to the same problems, but you should check it out anyway.
Using AsyncTask with progress dialogs and device rotation.
A working solution!
I have finally got everything to work. My code has the following features:
A Fragment whose layout changes with orientation.
An AsyncTask in which you can do some work.
A DialogFragment which shows the progress of the task in a progress bar (not just an indeterminate spinner).
Rotation works without interrupting the task or dismissing the dialog.
The back button dismisses the dialog and cancels the task (you can alter this behaviour fairly easily though).
I don't think that combination of workingness can be found anywhere else.
The basic idea is as follows. There is a MainActivity class which contains a single fragment - MainFragment. MainFragment has different layouts for horizontal and vertical orientation, and setRetainInstance() is false so that the layout can change. This means that when the device orientation is changed, both MainActivity and MainFragment are completely destroyed and recreated.
Separately we have MyTask (extended from AsyncTask) which does all the work. We can't store it in MainFragment because that will be destroyed, and Google has deprecated using anything like setRetainNonInstanceConfiguration(). That isn't always available anyway and is an ugly hack at best. Instead we will store MyTask in another fragment, a DialogFragment called TaskFragment. This fragment will have setRetainInstance() set to true, so as the device rotates this fragment isn't destroyed, and MyTask is retained.
Finally we need to tell the TaskFragment who to inform when it is finished, and we do that using setTargetFragment(<the MainFragment>) when we create it. When the device is rotated and the MainFragment is destroyed and a new instance is created, we use the FragmentManager to find the dialog (based on its tag) and do setTargetFragment(<the new MainFragment>). That's pretty much it.
There were two other things I needed to do: first cancel the task when the dialog is dismissed, and second set the dismiss message to null, otherwise the dialog is weirdly dismissed when the device is rotated.
The code
I won't list the layouts, they are pretty obvious and you can find them in the project download below.
MainActivity
This is pretty straightforward. I added a callback into this activity so it knows when the task is finished, but you might not need that. Mainly I just wanted to show the fragment-activity callback mechanism because it's quite neat and you might not have seen it before.
public class MainActivity extends Activity implements MainFragment.Callbacks
{
#Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
#Override
public void onTaskFinished()
{
// Hooray. A toast to our success.
Toast.makeText(this, "Task finished!", Toast.LENGTH_LONG).show();
// NB: I'm going to blow your mind again: the "int duration" parameter of makeText *isn't*
// the duration in milliseconds. ANDROID Y U NO ENUM?
}
}
MainFragment
It's long but worth it!
public class MainFragment extends Fragment implements OnClickListener
{
// This code up to onDetach() is all to get easy callbacks to the Activity.
private Callbacks mCallbacks = sDummyCallbacks;
public interface Callbacks
{
public void onTaskFinished();
}
private static Callbacks sDummyCallbacks = new Callbacks()
{
public void onTaskFinished() { }
};
#Override
public void onAttach(Activity activity)
{
super.onAttach(activity);
if (!(activity instanceof Callbacks))
{
throw new IllegalStateException("Activity must implement fragment's callbacks.");
}
mCallbacks = (Callbacks) activity;
}
#Override
public void onDetach()
{
super.onDetach();
mCallbacks = sDummyCallbacks;
}
// Save a reference to the fragment manager. This is initialised in onCreate().
private FragmentManager mFM;
// Code to identify the fragment that is calling onActivityResult(). We don't really need
// this since we only have one fragment to deal with.
static final int TASK_FRAGMENT = 0;
// Tag so we can find the task fragment again, in another instance of this fragment after rotation.
static final String TASK_FRAGMENT_TAG = "task";
#Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
// At this point the fragment may have been recreated due to a rotation,
// and there may be a TaskFragment lying around. So see if we can find it.
mFM = getFragmentManager();
// Check to see if we have retained the worker fragment.
TaskFragment taskFragment = (TaskFragment)mFM.findFragmentByTag(TASK_FRAGMENT_TAG);
if (taskFragment != null)
{
// Update the target fragment so it goes to this fragment instead of the old one.
// This will also allow the GC to reclaim the old MainFragment, which the TaskFragment
// keeps a reference to. Note that I looked in the code and setTargetFragment() doesn't
// use weak references. To be sure you aren't leaking, you may wish to make your own
// setTargetFragment() which does.
taskFragment.setTargetFragment(this, TASK_FRAGMENT);
}
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState)
{
return inflater.inflate(R.layout.fragment_main, container, false);
}
#Override
public void onViewCreated(View view, Bundle savedInstanceState)
{
super.onViewCreated(view, savedInstanceState);
// Callback for the "start task" button. I originally used the XML onClick()
// but it goes to the Activity instead.
view.findViewById(R.id.taskButton).setOnClickListener(this);
}
#Override
public void onClick(View v)
{
// We only have one click listener so we know it is the "Start Task" button.
// We will create a new TaskFragment.
TaskFragment taskFragment = new TaskFragment();
// And create a task for it to monitor. In this implementation the taskFragment
// executes the task, but you could change it so that it is started here.
taskFragment.setTask(new MyTask());
// And tell it to call onActivityResult() on this fragment.
taskFragment.setTargetFragment(this, TASK_FRAGMENT);
// Show the fragment.
// I'm not sure which of the following two lines is best to use but this one works well.
taskFragment.show(mFM, TASK_FRAGMENT_TAG);
// mFM.beginTransaction().add(taskFragment, TASK_FRAGMENT_TAG).commit();
}
#Override
public void onActivityResult(int requestCode, int resultCode, Intent data)
{
if (requestCode == TASK_FRAGMENT && resultCode == Activity.RESULT_OK)
{
// Inform the activity.
mCallbacks.onTaskFinished();
}
}
TaskFragment
// This and the other inner class can be in separate files if you like.
// There's no reason they need to be inner classes other than keeping everything together.
public static class TaskFragment extends DialogFragment
{
// The task we are running.
MyTask mTask;
ProgressBar mProgressBar;
public void setTask(MyTask task)
{
mTask = task;
// Tell the AsyncTask to call updateProgress() and taskFinished() on this fragment.
mTask.setFragment(this);
}
#Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
// Retain this instance so it isn't destroyed when MainActivity and
// MainFragment change configuration.
setRetainInstance(true);
// Start the task! You could move this outside this activity if you want.
if (mTask != null)
mTask.execute();
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState)
{
View view = inflater.inflate(R.layout.fragment_task, container);
mProgressBar = (ProgressBar)view.findViewById(R.id.progressBar);
getDialog().setTitle("Progress Dialog");
// If you're doing a long task, you probably don't want people to cancel
// it just by tapping the screen!
getDialog().setCanceledOnTouchOutside(false);
return view;
}
// This is to work around what is apparently a bug. If you don't have it
// here the dialog will be dismissed on rotation, so tell it not to dismiss.
#Override
public void onDestroyView()
{
if (getDialog() != null && getRetainInstance())
getDialog().setDismissMessage(null);
super.onDestroyView();
}
// Also when we are dismissed we need to cancel the task.
#Override
public void onDismiss(DialogInterface dialog)
{
super.onDismiss(dialog);
// If true, the thread is interrupted immediately, which may do bad things.
// If false, it guarantees a result is never returned (onPostExecute() isn't called)
// but you have to repeatedly call isCancelled() in your doInBackground()
// function to check if it should exit. For some tasks that might not be feasible.
if (mTask != null) {
mTask.cancel(false);
}
// You don't really need this if you don't want.
if (getTargetFragment() != null)
getTargetFragment().onActivityResult(TASK_FRAGMENT, Activity.RESULT_CANCELED, null);
}
#Override
public void onResume()
{
super.onResume();
// This is a little hacky, but we will see if the task has finished while we weren't
// in this activity, and then we can dismiss ourselves.
if (mTask == null)
dismiss();
}
// This is called by the AsyncTask.
public void updateProgress(int percent)
{
mProgressBar.setProgress(percent);
}
// This is also called by the AsyncTask.
public void taskFinished()
{
// Make sure we check if it is resumed because we will crash if trying to dismiss the dialog
// after the user has switched to another app.
if (isResumed())
dismiss();
// If we aren't resumed, setting the task to null will allow us to dimiss ourselves in
// onResume().
mTask = null;
// Tell the fragment that we are done.
if (getTargetFragment() != null)
getTargetFragment().onActivityResult(TASK_FRAGMENT, Activity.RESULT_OK, null);
}
}
MyTask
// This is a fairly standard AsyncTask that does some dummy work.
public static class MyTask extends AsyncTask<Void, Void, Void>
{
TaskFragment mFragment;
int mProgress = 0;
void setFragment(TaskFragment fragment)
{
mFragment = fragment;
}
#Override
protected Void doInBackground(Void... params)
{
// Do some longish task. This should be a task that we don't really
// care about continuing
// if the user exits the app.
// Examples of these things:
// * Logging in to an app.
// * Downloading something for the user to view.
// * Calculating something for the user to view.
// Examples of where you should probably use a service instead:
// * Downloading files for the user to save (like the browser does).
// * Sending messages to people.
// * Uploading data to a server.
for (int i = 0; i < 10; i++)
{
// Check if this has been cancelled, e.g. when the dialog is dismissed.
if (isCancelled())
return null;
SystemClock.sleep(500);
mProgress = i * 10;
publishProgress();
}
return null;
}
#Override
protected void onProgressUpdate(Void... unused)
{
if (mFragment == null)
return;
mFragment.updateProgress(mProgress);
}
#Override
protected void onPostExecute(Void unused)
{
if (mFragment == null)
return;
mFragment.taskFinished();
}
}
}
Download the example project
Here is the source code and the APK. Sorry, the ADT insisted on adding the support library before it would let me make a project. I'm sure you can remove it.
I've recently posted an article describing how to handle configuration changes using retained Fragments. It solves the problem of retaining an AsyncTask across a rotation change nicely.
The TL;DR is to use host your AsyncTask inside a Fragment, call setRetainInstance(true) on the Fragment, and report the AsyncTask's progress/results back to it's Activity (or it's target Fragment, if you choose to use the approach described by #Timmmm) through the retained Fragment.
My first suggestion is to avoid inner AsyncTasks, you can read a question that I asked about this and the answers: Android: AsyncTask recommendations: private class or public class?
After that i started using non-inner and... now i see A LOT of benefits.
The second is, keep a reference of your running AsyncTask in the Application Class - http://developer.android.com/reference/android/app/Application.html
Everytime you start an AsyncTask, set it on the Application and when it finishes it set it to null.
When a fragment/activity starts you can check if any AsyncTask is running (by checking if it's null or not on the Application) and then set the reference inside to whatever you want (activity, fragment etc so you can do callbacks).
This will solve your problem:
If you only have 1 AsyncTask running at any determined time you can add a simple reference:
AsyncTask<?,?,?> asyncTask = null;
Else, have in the Aplication a HashMap with references to them.
The progress dialog can follow the exact same principle.
I came up with a method of using AsyncTaskLoaders for this. It's pretty easy to use and requires less overhead IMO..
Basically you create an AsyncTaskLoader like this:
public class MyAsyncTaskLoader extends AsyncTaskLoader {
Result mResult;
public HttpAsyncTaskLoader(Context context) {
super(context);
}
protected void onStartLoading() {
super.onStartLoading();
if (mResult != null) {
deliverResult(mResult);
}
if (takeContentChanged() || mResult == null) {
forceLoad();
}
}
#Override
public Result loadInBackground() {
SystemClock.sleep(500);
mResult = new Result();
return mResult;
}
}
Then in your activity that uses the above AsyncTaskLoader when a button is clicked:
public class MyActivityWithBackgroundWork extends FragmentActivity implements LoaderManager.LoaderCallbacks<Result> {
private String username,password;
#Override
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
setContentView(R.layout.mylayout);
//this is only used to reconnect to the loader if it already started
//before the orientation changed
Loader loader = getSupportLoaderManager().getLoader(0);
if (loader != null) {
getSupportLoaderManager().initLoader(0, null, this);
}
}
public void doBackgroundWorkOnClick(View button) {
//might want to disable the button while you are doing work
//to prevent user from pressing it again.
//Call resetLoader because calling initLoader will return
//the previous result if there was one and we may want to do new work
//each time
getSupportLoaderManager().resetLoader(0, null, this);
}
#Override
public Loader<Result> onCreateLoader(int i, Bundle bundle) {
//might want to start a progress bar
return new MyAsyncTaskLoader(this);
}
#Override
public void onLoadFinished(Loader<LoginResponse> loginLoader,
LoginResponse loginResponse)
{
//handle result
}
#Override
public void onLoaderReset(Loader<LoginResponse> responseAndJsonHolderLoader)
{
//remove references to previous loader resources
}
}
This seems to handle orientation changes fine and your background task will continue during the rotation.
A few things to note:
If in onCreate you reattach to the asynctaskloader you will get called back in onLoadFinished() with the previous result (even if you had already been told the request was complete). This is actually good behavior most of the time but sometimes it can be tricky to handle. While I imagine there are lots of ways to handle this what I did was I called loader.abandon() in onLoadFinished. Then I added check in onCreate to only reattach to the loader if it wasn't already abandoned. If you need the resulting data again you won't want to do that. In most cases you want the data.
I have more details on using this for http calls here
I created a very tiny open-source background task library which is heavily based on the Marshmallow AsyncTask but with additional functionality such as:
Automatically retaining tasks across configuration changes;
UI callback (listeners);
Doesn't restart or cancel task when the device rotates (like Loaders would do);
The library internally uses a Fragment without any user interface, which is retained accross configuration changes (setRetainInstance(true)).
You can find it on GitHub: https://github.com/NeoTech-Software/Android-Retainable-Tasks
Most basic example (version 0.2.0):
This example fully retains the task, using a very limited amount of code.
Task:
private class ExampleTask extends Task<Integer, String> {
public ExampleTask(String tag){
super(tag);
}
protected String doInBackground() {
for(int i = 0; i < 100; i++) {
if(isCancelled()){
break;
}
SystemClock.sleep(50);
publishProgress(i);
}
return "Result";
}
}
Activity:
public class Main extends TaskActivityCompat implements Task.Callback {
#Override
public void onClick(View view){
ExampleTask task = new ExampleTask("activity-unique-tag");
getTaskManager().execute(task, this);
}
#Override
public Task.Callback onPreAttach(Task<?, ?> task) {
//Restore the user-interface based on the tasks state
return this; //This Activity implements Task.Callback
}
#Override
public void onPreExecute(Task<?, ?> task) {
//Task started
}
#Override
public void onPostExecute(Task<?, ?> task) {
//Task finished
Toast.makeText(this, "Task finished", Toast.LENGTH_SHORT).show();
}
}
My approach is to use delegation design pattern, in general, we can isolate the actual business logic (read data from internet or database or whatsoever) from AsyncTask (the delegator) to BusinessDAO (the delegate), in your AysncTask.doInBackground() method, delegate the actual task to BusinessDAO, then implement a singleton process mechanism in BusinessDAO, so that multiple call to BusinessDAO.doSomething() will just trigger one actual task running each time and waiting for the task result. The idea is retain the delegate (i.e. BusinessDAO) during the configuration change, instead of the delegator (i.e. AsyncTask).
Create/Implement our own Application, the purpose is to create/initialize BusinessDAO here, so that our BusinessDAO's lifecycle is application scoped, not activity scoped, note that you need change AndroidManifest.xml to use MyApplication:
public class MyApplication extends android.app.Application {
private BusinessDAO businessDAO;
#Override
public void onCreate() {
super.onCreate();
businessDAO = new BusinessDAO();
}
pubilc BusinessDAO getBusinessDAO() {
return businessDAO;
}
}
Our existing Activity/Fragement are mostly unchanged, still implement AsyncTask as an inner class and involve AsyncTask.execute() from Activity/Fragement, the difference now is AsyncTask will delegate the actual task to BusinessDAO, so during the configuration change, a second AsyncTask will be initialized and executed, and call BusinessDAO.doSomething() second time, however, second call to BusinessDAO.doSomething() will not trigger a new running task, instead, waiting for current running task to finish:
public class LoginFragment extends Fragment {
... ...
public class LoginAsyncTask extends AsyncTask<String, Void, Boolean> {
// get a reference of BusinessDAO from application scope.
BusinessDAO businessDAO = ((MyApplication) getApplication()).getBusinessDAO();
#Override
protected Boolean doInBackground(String... args) {
businessDAO.doSomething();
return true;
}
protected void onPostExecute(Boolean result) {
//Handle task result and update UI stuff.
}
}
... ...
}
Inside BusinessDAO, implement singleton process mechanism, for example:
public class BusinessDAO {
ExecutorCompletionService<MyTask> completionExecutor = new ExecutorCompletionService<MyTask(Executors.newFixedThreadPool(1));
Future<MyTask> myFutureTask = null;
public void doSomething() {
if (myFutureTask == null) {
// nothing running at the moment, submit a new callable task to run.
MyTask myTask = new MyTask();
myFutureTask = completionExecutor.submit(myTask);
}
// Task already submitted and running, waiting for the running task to finish.
myFutureTask.get();
}
// If you've never used this before, Callable is similar with Runnable, with ability to return result and throw exception.
private class MyTask extends Callable<MyTask> {
public MyAsyncTask call() {
// do your job here.
return this;
}
}
}
I am not 100% sure if this will work, moreover, the sample code snippet should be considered as pseudocode. I am just trying to give you some clue from design level. Any feedback or suggestions are welcome and appreciated.
You could make the AsyncTask a static field. If you need a context, you should ship your application context. This will avoid memory leaks, otherwise you'd keep a reference to your entire activity.
If anyone finds their way to this thread then I found a clean approach was to run the Async task from an app.Service (started with START_STICKY) and then on recreate iterate over the running services to find out whether the service (and hence async task) is still running;
public boolean isServiceRunning(String serviceClassName) {
final ActivityManager activityManager = (ActivityManager) Application.getContext().getSystemService(Context.ACTIVITY_SERVICE);
final List<RunningServiceInfo> services = activityManager.getRunningServices(Integer.MAX_VALUE);
for (RunningServiceInfo runningServiceInfo : services) {
if (runningServiceInfo.service.getClassName().equals(serviceClassName)){
return true;
}
}
return false;
}
If it is, re-add the DialogFragment (or whatever) and if it is not ensure the dialog has been dismissed.
This is particularly pertinent if you are using the v4.support.* libraries since (at the time of writing) they have know issues with the setRetainInstance method and view paging. Furthermore, by not retaining the instance you can recreate your activity using a different set of resources (i.e. a different view layout for the new orientation)
I write samepl code to solve this problem
First step is make Application class:
public class TheApp extends Application {
private static TheApp sTheApp;
private HashMap<String, AsyncTask<?,?,?>> tasks = new HashMap<String, AsyncTask<?,?,?>>();
#Override
public void onCreate() {
super.onCreate();
sTheApp = this;
}
public static TheApp get() {
return sTheApp;
}
public void registerTask(String tag, AsyncTask<?,?,?> task) {
tasks.put(tag, task);
}
public void unregisterTask(String tag) {
tasks.remove(tag);
}
public AsyncTask<?,?,?> getTask(String tag) {
return tasks.get(tag);
}
}
In AndroidManifest.xml
<application
android:allowBackup="true"
android:icon="#drawable/ic_launcher"
android:label="#string/app_name"
android:theme="#style/AppTheme"
android:name="com.example.tasktest.TheApp">
Code in activity:
public class MainActivity extends Activity {
private Task1 mTask1;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTask1 = (Task1)TheApp.get().getTask("task1");
}
/*
* start task is not running jet
*/
public void handletask1(View v) {
if (mTask1 == null) {
mTask1 = new Task1();
TheApp.get().registerTask("task1", mTask1);
mTask1.execute();
} else
Toast.makeText(this, "Task is running...", Toast.LENGTH_SHORT).show();
}
/*
* cancel task if is not finished
*/
public void handelCancel(View v) {
if (mTask1 != null)
mTask1.cancel(false);
}
public class Task1 extends AsyncTask<Void, Void, Void>{
#Override
protected Void doInBackground(Void... params) {
try {
for(int i=0; i<120; i++) {
Thread.sleep(1000);
Log.i("tests", "loop=" + i);
if (this.isCancelled()) {
Log.e("tests", "tssk cancelled");
break;
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}
#Override
protected void onCancelled(Void result) {
TheApp.get().unregisterTask("task1");
mTask1 = null;
}
#Override
protected void onPostExecute(Void result) {
TheApp.get().unregisterTask("task1");
mTask1 = null;
}
}
}
When activity orientation changes variable mTask is inited from app context. When task is finished variable is set to null and remove from memory.
For me its enough.
Have a look at below example , how to use retained fragment to retain background task:
public class NetworkRequestFragment extends Fragment {
// Declare some sort of interface that your AsyncTask will use to communicate with the Activity
public interface NetworkRequestListener {
void onRequestStarted();
void onRequestProgressUpdate(int progress);
void onRequestFinished(SomeObject result);
}
private NetworkTask mTask;
private NetworkRequestListener mListener;
private SomeObject mResult;
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
// Try to use the Activity as a listener
if (activity instanceof NetworkRequestListener) {
mListener = (NetworkRequestListener) activity;
} else {
// You can decide if you want to mandate that the Activity implements your callback interface
// in which case you should throw an exception if it doesn't:
throw new IllegalStateException("Parent activity must implement NetworkRequestListener");
// or you could just swallow it and allow a state where nobody is listening
}
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Retain this Fragment so that it will not be destroyed when an orientation
// change happens and we can keep our AsyncTask running
setRetainInstance(true);
}
/**
* The Activity can call this when it wants to start the task
*/
public void startTask(String url) {
mTask = new NetworkTask(url);
mTask.execute();
}
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// If the AsyncTask finished when we didn't have a listener we can
// deliver the result here
if ((mResult != null) && (mListener != null)) {
mListener.onRequestFinished(mResult);
mResult = null;
}
}
#Override
public void onDestroy() {
super.onDestroy();
// We still have to cancel the task in onDestroy because if the user exits the app or
// finishes the Activity, we don't want the task to keep running
// Since we are retaining the Fragment, onDestroy won't be called for an orientation change
// so this won't affect our ability to keep the task running when the user rotates the device
if ((mTask != null) && (mTask.getStatus == AsyncTask.Status.RUNNING)) {
mTask.cancel(true);
}
}
#Override
public void onDetach() {
super.onDetach();
// This is VERY important to avoid a memory leak (because mListener is really a reference to an Activity)
// When the orientation change occurs, onDetach will be called and since the Activity is being destroyed
// we don't want to keep any references to it
// When the Activity is being re-created, onAttach will be called and we will get our listener back
mListener = null;
}
private class NetworkTask extends AsyncTask<String, Integer, SomeObject> {
#Override
protected void onPreExecute() {
if (mListener != null) {
mListener.onRequestStarted();
}
}
#Override
protected SomeObject doInBackground(String... urls) {
// Make the network request
...
// Whenever we want to update our progress:
publishProgress(progress);
...
return result;
}
#Override
protected void onProgressUpdate(Integer... progress) {
if (mListener != null) {
mListener.onRequestProgressUpdate(progress[0]);
}
}
#Override
protected void onPostExecute(SomeObject result) {
if (mListener != null) {
mListener.onRequestFinished(result);
} else {
// If the task finishes while the orientation change is happening and while
// the Fragment is not attached to an Activity, our mListener might be null
// If you need to make sure that the result eventually gets to the Activity
// you could save the result here, then in onActivityCreated you can pass it back
// to the Activity
mResult = result;
}
}
}
}
Have a look here.
There is a solution based on Timmmm's solution.
But I improved it:
Now the solution is extendable - you only need to extend FragmentAbleToStartTask
You able to keep running several tasks at the same time.
And in my opinion it's as easy as startActivityForResult and receive result
You also can stop a running task and check whether particular task is running
Sorry for my English

Fragments, DialogFragment, and Screen Rotation

I have an Activity that calls setContentView with this XML:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="horizontal"
>
<fragment android:name="org.vt.indiatab.GroupFragment"
android:id="#+id/home_groups"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_weight="1" />
<..some other fragments ...>
</LinearLayout>
The GroupFragment extends Fragment, and all is well there. However, I show a DialogFragment from within GroupFragment. This shows correctly, HOWEVER when the screen rotates, I get a Force Close.
What's the proper way to display a DialogFragment from within another Fragment other than DialogFragment.show(FragmentManager, String)?
There's a bug in the compatibility library that can cause this. Try putting this in you dialogfragment:
#Override
public void onDestroyView() {
if (getDialog() != null && getRetainInstance())
getDialog().setOnDismissListener(null);
super.onDestroyView();
}
I also suggest setting your dialogfragment as retained, so it won't get dismissed after the rotation. Put "setRetainInstance(true);" e.g. in the onCreate() method.
OK, while Zsombor's method works, this is due to me being inexperienced with Fragments and his solution causes issues with the saveInstanceState Bundle.
Apparently (at least for a DialogFragment), it should be a public static class. You also MUST write your own static DialogFragment newInstance() method. This is because the Fragment class calls the newInstance method in its instantiate() method.
So in conclusion, you MUST write your DialogFragments like so:
public static class MyDialogFragment extends DialogFragment {
static MyDialogFragment newInstance() {
MyDialogFragment d = new MyDialogFragment();
return d;
}
#Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
...
}
}
And show them with:
private void showMyDialog() {
MyDialogFragment d = MyDialogFragment.newInstance();
d.show(getFragmentManager(), "dialog");
}
This may be unique to the ActionBarSherlock Library, but the official samples in the SDK documentation use this paradigm also.
To overcome the Bundle always being null, I save it to a static field in onSaveInstanceState. It's a code smell, but the only solution I found for both restoring the dialog and saving the state.
The Bundle reference should be nulled in onDestroy.
#Override
public void onCreate(Bundle savedInstanceState)
{
if (savedInstanceState == null)
savedInstanceState = HackishSavedState.savedInstanceState;
setRetainInstance(true);
}
#Override
public Dialog onCreateDialog(Bundle savedInstanceState)
{
if (savedInstanceState == null)
savedInstanceState = HackishSavedState.savedInstanceState;
...
}
#Override
public void onDestroyView() // necessary for restoring the dialog
{
if (getDialog() != null && getRetainInstance())
getDialog().setOnDismissListener(null);
super.onDestroyView();
}
#Override
public void onSaveInstanceState(Bundle outState)
{
...
HackishSavedState.savedInstanceState = outState;
super.onSaveInstanceState(outState);
}
#Override
public void onDestroy()
{
HackishSavedState.savedInstanceState = null;
super.onDestroy();
}
private static class HackishSavedState
{
static Bundle savedInstanceState;
}
I used a mix of the presented solutions and added one more thing.
This is my final solution:
I used setRetainInstance(true) in the onCreateDialog;
I used this:
public void onDestroyView() {
if (getDialog() != null && getRetainInstance())
getDialog().setDismissMessage(null);
super.onDestroyView();
}
And as a workaround of the savedInstanceState not working, I created a private class called StateHolder (the same way a holder is create for a listView):
private class StateHolder {
String name;
String quantity;
}
I save the state this way:
#Override
public void onSaveInstanceState(Bundle savedInstanceState) {
super.onSaveInstanceState(savedInstanceState);
stateHolder = new StateHolder();
stateHolder.name = actvProductName.getText().toString();
stateHolder.quantity = etProductQuantity.getText().toString();
}
In the onDismiss method I set the stateHolder back to null. When the dialog is created, it verifies if the stateHolder isn't null to recover the state or just initialize everything normally.
I solved this issue with answers of #ZsomborErdődy-Nagy and #AndyDennie . You must subclass this class and in you parent fragment call setRetainInstance(true), and dialogFragment.show(getFragmentManager(), "Dialog");
public class AbstractDialogFragment extends DialogFragment {
#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 had a similar issue, however none of the above worked for me. In the end I needed to create the fragment in code instead of in the XML layout.
See: Replacing fragments and orientation change
I ran into this on my project and none of the above solutions helped.
If the exception looks something like
java.lang.RuntimeException: Unable to start activity ComponentInfo{
...
Caused by: java.lang.IllegalStateException: Fragment....
did not create a view.
It's caused by an issue with a fallback container Id that gets used after rotation. See this ticket for more details:
https://code.google.com/p/android/issues/detail?id=18529
Basically you can prevent the crash by making sure all of your xml fragments have a tag defined in the layout. This prevents the fallback condition from occurring if you rotate when a fragment is visible.
In my case I was able to apply this fix without having to override onDestroyView() or setRetainInstance(true), which is the common recommendation for this situation.
I encountered this problem and the onDestroyView() trick wasn't working. It turned out that it was because I was doing some rather intensive dialog creation in onCreate(). This included saving a reference to the AlertDialog, which I would then return in onCreateDialog().
When I moved all of this code to onCreateDialog() and stopped retaining a reference to the dialog, it started working again. I expect I was violating one of the invariants DialogFragment has about managing its dialog.
In onCreate() call setRetainInstance(true) and then include this:
#Override
public void onDestroyView() {
if (getDialog() != null && getRetainInstance()) {
getDialog().setOnDismissMessage(null);
}
super.onDestroyView();
}
When you call setRetainInstance(true) in onCreate(), onCreate() will no longer be called across orientation changes, but onCreateView() will still be called.
So you can still save the state to your bundle in onSaveInstanceState() and then retrieve it in onCreateView():
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt("myInt", myInt);
}
#Override
public View onCreateView(LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.my_layout, container);
if (savedInstanceState != null) {
myInt = savedInstanceState.getInt("myInt");
}
...
return view;
}

Categories

Resources