Felipe Caldas
Felipe Caldas

Reputation: 2503

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

Upvotes: 8

Views: 8702

Answers (5)

Dante
Dante

Reputation: 21

on some android6.0 phones, I found that I had to request feature before super.onCreate(savedInstanceState)

Upvotes: 0

Heath Borders
Heath Borders

Reputation: 32157

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
}

Upvotes: 3

Xaver Kapeller
Xaver Kapeller

Reputation: 49817

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:

  1. 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.

  1. 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();
        }
    }
    
  2. 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.

Upvotes: 5

Felipe Caldas
Felipe Caldas

Reputation: 2503

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

Upvotes: -1

Felipe Caldas
Felipe Caldas

Reputation: 2503

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.

Upvotes: -1

Related Questions