AndroidRuntimeException:requestFeature() must be called before adding content in DialogFragment
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
andMyDialog
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()
inMapDialogFragmentv2
in the correct lifecycle methods inMyDialog
. 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 theonCreate()
method of yourMapDialogFragmentv2
after thesuper.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 beforeFragment.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
}