androidandroid-fragmentscommonsware

Unable to pause activity, content view not yet created


I grabbed the EU4You sample project demonstrated in The Busy Coder's Guide to Android Development 4.2 (courtesy of Mark Murphy aka commonsware here on StackOverflow). It may be easier to follow along with this sample project, but I will try to post relevant portions of code in the question. You do need to import the ActionBarSherlock project as well. You'll aslo need to set the Java compiler to 1.6 (to allow for the @Override annotation).

NOTE: I've changed the min-sdk to 8 (2.3, Froyo) and the target-sdk to 16 (4.1, Ice Cream Sandwich).

Note: I've changed the countries framelayout id to left_pane and the details framelayout id to right_pane.

Main layout file (layout-large-land):

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="horizontal"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent">
  <FrameLayout
    android:id="@+id/left_pane"
    android:layout_weight="30"
    android:layout_width="0px"
    android:layout_height="fill_parent"
  />
  <FrameLayout
    android:id="@+id/right_pane"
    android:layout_weight="70"
    android:layout_width="0px"
    android:layout_height="fill_parent"
  />
</LinearLayout>

Main layout file:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/left_pane"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent"
/>

The original version of the sample application has two activities (SherlockFragmentActivities to be exact).

  1. EU4You - Loads up the appropriate layout (default or landscape-large). If default, loads a list of countries in a SherlockListFragment. If landscape-large, loads up the list and a details fragment to display the selected country.
  2. DetailsActivity - Loads up the selected country via the DetailsFragment when not using the landscape-large layout.

If you click on a country in the list fragment, EU4You (the main hosting activity) checks for the existence and visibility of the DetailsFragment. If it exists, it loads up the country website inside that fragment. If not, it fires off the DetailsActivity, which then handles adding the DetailsFragment (which then loads the website for the given country).

This works fine, like a typical application supporting multiple screen layouts on tablets and handsets.

My problem is that I want to remove the DetailsActivity and stick to a single activity. I want to swap out (replace) the list fragment with the details fragment whenever I am not in the large landscape layout (in other words, I am either in a portrait layout on a tablet, or on a handset in any orientation). So what I have done, is changed the following in the EU4You SherlockFragmentActivity:

Original (with slight naming modification on the framelayout names mentioned in the note further up... countries is left_pane, details is right_pane):

@Override
public void onCountrySelected(Country c) {
    String url=getString(c.url);

    if (details != null && details.isVisible()) {
        details.loadUrl(url);
    }
    else {
        Intent i=new Intent(this, DetailsActivity.class);

        i.putExtra(DetailsActivity.EXTRA_URL, url);
        startActivity(i);
    }
}

My modified single activity version:

@Override
public void onCountrySelected(Country c) {
    String url=getString(c.url);

    if (details != null && details.isVisible()) {
        details.loadUrl(url);
    } 
    else {
        details = new DetailsFragment();
        FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
        ft.replace(R.id.left_pane, details);
        ft.addToBackStack(null);
        ft.commit();
        getSupportFragmentManager().executePendingTransactions();
        details.loadUrl(url);
    }
}

This works as expected. I click on a website on the list (in portrait mode on a tablet, or on a handset in any orientation) and the details show in the details fragment, which replaces the website list fragment. However, if I then click the home button on the device, while the country details are showing, I get a force close error, with the following logcat details (only with the default layout with just the single left_pane framelayout... I don't have this issue in landscape orientation on tablets, when there is a left_pane and right_pane):

11-16 15:04:33.525: E/AndroidRuntime(688): FATAL EXCEPTION: main
11-16 15:04:33.525: E/AndroidRuntime(688): java.lang.RuntimeException: Unable to pause activity {com.commonsware.android.eu4you/com.commonsware.android.eu4you.EU4You}: java.lang.IllegalStateException: Content view not yet created
11-16 15:04:33.525: E/AndroidRuntime(688):  at android.app.ActivityThread.performPauseActivity(ActivityThread.java:3348)
11-16 15:04:33.525: E/AndroidRuntime(688):  at android.app.ActivityThread.performPauseActivity(ActivityThread.java:3305)
11-16 15:04:33.525: E/AndroidRuntime(688):  at android.app.ActivityThread.handlePauseActivity(ActivityThread.java:3288)
11-16 15:04:33.525: E/AndroidRuntime(688):  at android.app.ActivityThread.access$2500(ActivityThread.java:125)
11-16 15:04:33.525: E/AndroidRuntime(688):  at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2040)
11-16 15:04:33.525: E/AndroidRuntime(688):  at android.os.Handler.dispatchMessage(Handler.java:99)
11-16 15:04:33.525: E/AndroidRuntime(688):  at android.os.Looper.loop(Looper.java:123)
11-16 15:04:33.525: E/AndroidRuntime(688):  at android.app.ActivityThread.main(ActivityThread.java:4627)
11-16 15:04:33.525: E/AndroidRuntime(688):  at java.lang.reflect.Method.invokeNative(Native Method)
11-16 15:04:33.525: E/AndroidRuntime(688):  at java.lang.reflect.Method.invoke(Method.java:521)
11-16 15:04:33.525: E/AndroidRuntime(688):  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:868)
11-16 15:04:33.525: E/AndroidRuntime(688):  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:626)
11-16 15:04:33.525: E/AndroidRuntime(688):  at dalvik.system.NativeStart.main(Native Method)
11-16 15:04:33.525: E/AndroidRuntime(688): Caused by: java.lang.IllegalStateException: Content view not yet created
11-16 15:04:33.525: E/AndroidRuntime(688):  at android.support.v4.app.ListFragment.ensureList(ListFragment.java:328)
11-16 15:04:33.525: E/AndroidRuntime(688):  at android.support.v4.app.ListFragment.getListView(ListFragment.java:222)
11-16 15:04:33.525: E/AndroidRuntime(688):  at com.commonsware.android.eu4you.CountriesFragment.onSaveInstanceState(CountriesFragment.java:64)
11-16 15:04:33.525: E/AndroidRuntime(688):  at android.support.v4.app.FragmentManagerImpl.saveFragmentBasicState(FragmentManager.java:1574)
11-16 15:04:33.525: E/AndroidRuntime(688):  at android.support.v4.app.FragmentManagerImpl.saveAllState(FragmentManager.java:1644)
11-16 15:04:33.525: E/AndroidRuntime(688):  at android.support.v4.app.FragmentActivity.onSaveInstanceState(FragmentActivity.java:499)
11-16 15:04:33.525: E/AndroidRuntime(688):  at com.actionbarsherlock.app.SherlockFragmentActivity.onSaveInstanceState(SherlockFragmentActivity.java:127)
11-16 15:04:33.525: E/AndroidRuntime(688):  at android.app.Activity.performSaveInstanceState(Activity.java:1036)
11-16 15:04:33.525: E/AndroidRuntime(688):  at android.app.Instrumentation.callActivityOnSaveInstanceState(Instrumentation.java:1180)
11-16 15:04:33.525: E/AndroidRuntime(688):  at android.app.ActivityThread.performPauseActivity(ActivityThread.java:3330)
11-16 15:04:33.525: E/AndroidRuntime(688):  ... 12 more

I can stop this error from occurring, if I remove the following line from the code above:

ft.addToBackStack(null);

I do require to be able to add the replace transaction to the backstack, so I can't remove this line (this sample application is the basis for my real application that has more stuff, but I used it to simplify my issue).

So two questions:

  1. Why is this happening?
  2. How can I fix it (perhaps another approach)?

One thing to note is that in my real application, this logic must all fall inside of tabs in an action bar (ActionBarSherlock since I must support back to version 2.3 (Froyo)).


UPDATE (2012-Nov-22 13:27 EST)

I've posted a sample project on GitHub. I'm new to Eclipse and Android development, so I may not have included everything I needed (workspace was not included and I don't think the ActionBarSherlock project is included... it would need to be imported (can be found here)).


Solution

  • I cannot comment on your specific crash, as I have not seen that error, other than to surmise that it is tied to replace() rather than using attach()/detach() (just a guess).

    However, getSupportFragmentManager().executePendingTransactions(); to me is a code smell. My guess is that you are doing this for the benefit of details.loadUrl(url);. Instead, please add a data member in DetailsFragment, something like urlToBeApplied, and have loadUrl() stash the value there if the WebView does not yet exist. It can then apply the URL in onCreateView(). Whether this will help with respect to your crash, I cannot say.

    You may wish to consider posting a complete sample project that demonstrates the issue, as you are counting on people recognizing the error message.


    UPDATE

    Based on the sample app, I was able to better diagnose the problem.

    Content view not yet created, on a ListFragment, will mean that we are trying to call getListView() for a fragment that has not yet gone through onCreateView() or has gone through onDestroyView(). This usually indicates a timing problem.

    In this case, as it turns out, the work that was crashing was superfluous, anyway. onSaveInstanceState() is trying to hold onto the checked item position, so we can re-check it later (e.g., after a configuration change). However, we do not even need to be in checked mode in the first place when we are on normal displays. The checked mode is for the activated state, which is relevant when then list and details are side-by-side, but is irrelevant when they are not. So, the workaround is to simply not bother with the onSaveInstanceState() logic if we are not needing a persistent selection... which we can easily get by calling isPersistentSelection() on our listener.

    For the sake of argument, though, let's pretend that we did need to do this work. In that case, we have a couple of options. The simplest probably is to override onDestroyView() and cache the checked item position, using that in onSaveInstanceState() if getListView() returns null.