3

I don't understand the Fragment lifecycle in Android, and what happens during screen orientation changes.

I started with the Master-Detail example in the Android SDK, and I have added the following lines of code:

in MyItemListActivity I modified onCreate()

public class MyItemListActivity extends FragmentActivity implements
    MyItemListFragment.Callbacks {

/**
 * Whether or not the activity is in two-pane mode, i.e. running on a tablet
 * device.
 */
private boolean mTwoPane;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    FragmentManager fragMgr = getSupportFragmentManager();
    MyItemListFragment oldFragment = (MyItemListFragment)fragMgr.findFragmentByTag("booFragment");

    if (null == oldFragment) {
        FragmentTransaction xact = fragMgr.beginTransaction();
        MyItemListFragment newFragment = MyItemListFragment.createInstance("boo");
        xact.add(
            R.id.myitem_list,
            newFragment,
            "booFragment");
        xact.commit();        
    }

    setContentView(R.layout.activity_myitem_list);

    if (findViewById(R.id.myitem_detail_container) != null) {
        // The detail container view will be present only in the
        // large-screen layouts (res/values-large and
        // res/values-sw600dp). If this view is present, then the
        // activity should be in two-pane mode.
        mTwoPane = true;

        // In two-pane mode, list items should be given the
        // 'activated' state when touched.
        ((MyItemListFragment) getSupportFragmentManager().findFragmentById(
                R.id.myitem_list)).setActivateOnItemClick(true);
    }

    // TODO: If exposing deep links into your app, handle intents here.
}

/**
 * Callback method from {@link MyItemListFragment.Callbacks} indicating that
 * the item with the given ID was selected.
 */
@Override
public void onItemSelected(String id) {
    if (mTwoPane) {
        // In two-pane mode, show the detail view in this activity by
        // adding or replacing the detail fragment using a
        // fragment transaction.
        Bundle arguments = new Bundle();
        arguments.putString(MyItemDetailFragment.ARG_ITEM_ID, id);
        MyItemDetailFragment fragment = new MyItemDetailFragment();
        fragment.setArguments(arguments);
        getSupportFragmentManager().beginTransaction()
                .replace(R.id.myitem_detail_container, fragment).commit();

    } else {
        // In single-pane mode, simply start the detail activity
        // for the selected item ID.
        Intent detailIntent = new Intent(this, MyItemDetailActivity.class);
        detailIntent.putExtra(MyItemDetailFragment.ARG_ITEM_ID, id);
        startActivity(detailIntent);
    }
}
}

in MyItemListFragment I created createInstance()

public class MyItemListFragment extends ListFragment {

/**
 * The serialization (saved instance state) Bundle key representing the
 * activated item position. Only used on tablets.
 */
private static final String STATE_ACTIVATED_POSITION = "activated_position";

/**
 * The fragment's current callback object, which is notified of list item
 * clicks.
 */
private Callbacks mCallbacks = sDummyCallbacks;

/**
 * The current activated item position. Only used on tablets.
 */
private int mActivatedPosition = ListView.INVALID_POSITION;

/**
 * A callback interface that all activities containing this fragment must
 * implement. This mechanism allows activities to be notified of item
 * selections.
 */
public interface Callbacks {
    /**
     * Callback for when an item has been selected.
     */
    public void onItemSelected(String id);
}

/**
 * A dummy implementation of the {@link Callbacks} interface that does
 * nothing. Used only when this fragment is not attached to an activity.
 */
private static Callbacks sDummyCallbacks = new Callbacks() {
    @Override
    public void onItemSelected(String id) {
    }
};

/**
 * Mandatory empty constructor for the fragment manager to instantiate the
 * fragment (e.g. upon screen orientation changes).
 */
public MyItemListFragment() {
}

public static MyItemListFragment createInstance(String boo) {
    Bundle init = new Bundle();
    init.putString(
        "booboo",
        boo);

    MyItemListFragment frag = new MyItemListFragment();
    frag.setArguments(init);
    return frag;
}

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // TODO: replace with a real list adapter.
    setListAdapter(new ArrayAdapter<DummyContent.DummyItem>(getActivity(),
            android.R.layout.simple_list_item_activated_1,
            android.R.id.text1, DummyContent.ITEMS));
}

@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);

    // Restore the previously serialized activated item position.
    if (savedInstanceState != null
            && savedInstanceState.containsKey(STATE_ACTIVATED_POSITION)) {
        setActivatedPosition(savedInstanceState
                .getInt(STATE_ACTIVATED_POSITION));
    }
}

@Override
public void onAttach(Activity activity) {
    super.onAttach(activity);

    // Activities containing this fragment must implement its callbacks.
    if (!(activity instanceof Callbacks)) {
        throw new IllegalStateException(
                "Activity must implement fragment's callbacks.");
    }

    mCallbacks = (Callbacks) activity;
}

@Override
public void onDetach() {
    super.onDetach();

    // Reset the active callbacks interface to the dummy implementation.
    mCallbacks = sDummyCallbacks;
}

@Override
public void onListItemClick(ListView listView, View view, int position,
        long id) {
    super.onListItemClick(listView, view, position, id);

    // Notify the active callbacks interface (the activity, if the
    // fragment is attached to one) that an item has been selected.
    mCallbacks.onItemSelected(DummyContent.ITEMS.get(position).id);
}

@Override
public void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    if (mActivatedPosition != ListView.INVALID_POSITION) {
        // Serialize and persist the activated item position.
        outState.putInt(STATE_ACTIVATED_POSITION, mActivatedPosition);
    }
}

/**
 * Turns on activate-on-click mode. When this mode is on, list items will be
 * given the 'activated' state when touched.
 */
public void setActivateOnItemClick(boolean activateOnItemClick) {
    // When setting CHOICE_MODE_SINGLE, ListView will automatically
    // give items the 'activated' state when touched.
    getListView().setChoiceMode(
            activateOnItemClick ? ListView.CHOICE_MODE_SINGLE
                    : ListView.CHOICE_MODE_NONE);
}

private void setActivatedPosition(int position) {
    if (position == ListView.INVALID_POSITION) {
        getListView().setItemChecked(mActivatedPosition, false);
    } else {
        getListView().setItemChecked(position, true);
    }

    mActivatedPosition = position;
}
}

The app runs fine when started, but when I rotate the screen the app crashes and the following is in the logs:

12-12 13:41:23.930: E/AndroidRuntime(31051): java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.mymasterdetail/com.example.mymasterdetail.MyItemListActivity}: android.view.InflateException: Binary XML file line #24: Error inflating class fragment

...

12-12 13:41:23.930: E/AndroidRuntime(31051): Caused by: android.view.InflateException: Binary XML file line #24: Error inflating class fragment 12-12 13:41:23.930: E/AndroidRuntime(31051): at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:713)

...

12-12 13:41:23.930: E/AndroidRuntime(31051): at com.android.internal.policy.impl.PhoneWindow.setContentView(PhoneWindow.java:290) 12-12 13:41:23.930: E/AndroidRuntime(31051): at android.app.Activity.setContentView(Activity.java:1928) 12-12 13:41:23.930: E/AndroidRuntime(31051): at com.example.mymasterdetail.MyItemListActivity.onCreate(MyItemListActivity.java:52)

...

(Line 52 is the call to setContentView() in the Activity.)

If I remove xact.add() then the app runs just fine. (But no data is passed to the Fragment.)

I know that the FragmentTransaction approach is correct to pass data to my Fragment, but I don't see what else I need to do to prepare the Fragment to handle the lifecycle events associated with changes in screen orientation, and I don't know how to inflate the Fragment (either implicitly or explicitly.)

(I'm using a tablet, so I have the twoPane display, in case that makes a difference.)

  • see http://stackoverflow.com/questions/20413856/properly-interacting-with-fragments-in-fragmentpageradapter/20414354#20414354 see "OnSaveinstanceState()" you need to store the bundle that your trying to pass in xact.add in a way that the api will retreive it with the fragment that the data belongs to on orientation change. Use the orientation change api to pass ur data , not an explicit call. – Robert Rowntree Dec 12 '13 at 23:57

3 Answers3

0

Try moving your setContentView() to BEFORE you start instantiating your fragments.

If you setup your fragments first and try to attach them to views that don't yet exist then errors are likely.

Also ensure that the view you attach your fragments to exists in both your portrait and landscape layouts.

As you are adding your fragments programmatically, ensure you do not add them in your XML layouts as well. You should only have a FrameLayout in your layout to attach your fragment to.

Kuffs
  • 35,581
  • 10
  • 79
  • 92
  • That doesn't have any effect. Still crashes. – AnotherStupidUser Dec 12 '13 at 22:50
  • Then the problem is not likely in the code you have posted which is pretty standard (apart from the setContentView issue already mentioned) Perhaps you should post your complete code rather than just the snippets. – Kuffs Dec 12 '13 at 22:55
  • That's the only code I wrote. Otherwise I've taken the SDK example code and left it unmodified. – AnotherStupidUser Dec 12 '13 at 22:58
  • As the SDK code works and yours doesn't, the sensible thing would be to look at the differences and what you changed. – Kuffs Dec 12 '13 at 23:01
  • Your last comment clued me in - "As you are adding your fragments programmatically, ensure you do not add them in your XML layouts as well." I checked to see if the findFragmentById exists, and if it does I call `xact.replace()` rather than `xact.add()`. Thanks. – AnotherStupidUser Dec 13 '13 at 18:22
0

What happens during an orientation change is that the current activity is destroyed and recreated - this is useful, because alternate layouts can be inflated (if you have another one for a different orientation in your resources folder).

Likely what's happening here is that you're trying to access a fragment before it's been inflated (as Kuffs said).

Jade McGough
  • 348
  • 2
  • 13
0

Your fragment needs a public empty constructor.

From http://developer.android.com/reference/android/app/Fragment.html:

All subclasses of Fragment must include a public empty constructor. The framework will often re-instantiate a fragment class when needed, in particular during state restore, and needs to be able to find this constructor to instantiate it. If the empty constructor is not available, a runtime exception will occur in some cases during state restore.

Steve Prentice
  • 23,230
  • 11
  • 54
  • 55