2

I've found a ton of conflicting information regarding the proper way to restore application state when using Fragments embedded in Activities. Please let me know if my architecture is the problem because that is totally possible. My test Weather app is architected as follows.

The main activity "ReportsActivity" contains the fragment "ReportsFragment" (This is a list of the next 10 days of Weather Reports) ReportsFragment has an onItemClickListener that launches a new Activity "WeatherDetailActivity" and passes it an intent which contains some JSON Data that I use to populate the Weather Detail UI. This data is then presented on a fragment that the WeatherDetailActivity manages.

My problem is, when the user presses the back button, the ReportsFragment has been destroyed so it runs through its full lifecycle. I've tried a number of techniques I've found online to load the activity's data from a bundle, but no matter what I've tried so far the Intents' Extras are null in the ReportsActivity's onCreate method. (Note: the reason I need to do this is to avoid firing off an API Call each time I open my main Activity which fetches weather data from Weather Underground).

I'm struggling determining what would be the best way to construct this app: Should I have a single activity that pushes and pops Fragments that it manages? Or are multiple activities that each manage their own fragments the standard practice?

At the moment here is how I'm attempting to save my application state onto the intent. I'm trying to save the state in onPostExecute from my AsyncTask so i'm on the main thread after i've fetched my results from the API Call:

 @Override
protected void onPostExecute(Report[] result){
    if (result != null){

        ArrayList<String>reportsArrayList = new ArrayList<String>();
        Gson jsonArray = new GsonBuilder().setPrettyPrinting().create();
        for (int x = 0; x < result.length; x++){

            reportsArrayList.add(jsonArray.toJson(result[x], Report.class));
        }

        mExtras.putStringArrayList(ReportsActivity.ReportsActivityState.KEY_ACTIVITY_REPORTS,reportsArrayList);
    }
}

I then attempt to restore state from the ReportsActivity's onCreate Method:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_reports);

    if (savedInstanceState == null) {

        Intent intent = getIntent();

        mFragment = ReportsFragment.newInstance(intent
                .getStringArrayListExtra(ReportsActivityState.KEY_ACTIVITY_REPORTS));

        getFragmentManager().beginTransaction()
                .add(R.id.container, mFragment).commit();
    }
}

In all cases the StringArrayListExtra I'm trying to get from the intent return null.

This could very well be me trying to solve an Android problem with an iOS mindset, but is there not an easy way to just restore the main activity to what it was before I pushed the detail view?

altyus
  • 606
  • 4
  • 13

2 Answers2

1

I think it would be worth your while taking a look at EventBus.

Basically you can define a object holder of any kind, for example:

class WeatherData {
    List<String> reports;
    public WeatherData(List<String> reports) {
        this.reports = reports;
    }
}

Now, in an Activity or Fragment in which you wish to remember the state, or pass some state to another Activity or Fragment do:

// this removes all the hazzle of creating bundles etc
EventBus.getDefault().postSticky(new WeatherData(reports)); 

And any where in your code your wish to know the most recent WeatherData:

WeatherData weatherData = EventBus.getDefault().getSticky(WeatherData.class);

EventBus also has nice methods for event handling (button clicks, completion of long running processes, etc..)

The library can be found here: https://github.com/greenrobot/EventBus

And some more examples here: http://awalkingcity.com/blog/2013/02/26/productive-android-eventbus/

Some suggestions without using 3. part library:

1) Calling setRetainInstance(true) in your fragments onCreate method, what this should do is to persist public variables between instances.

Though it seems it does not work for fragments on the back stack: Understanding Fragment's setRetainInstance(boolean)

2) Hand the fragment data to your Activity, something like reading/updating ((YourActivity)getActivity()).someFragmentBundle, possibly save it in onSaveInstanceState of the Activity and retrieve it in onCreate. That is, having your Activity hold the data in-between instances.

3) You could also persist the data, saving it to a file or using SharedPreferences http://developer.android.com/training/basics/data-storage/index.html

This method has the advantage that it will enable restoring the data even after a complete kill of your app.

The Architectural question

Disclaimer: subjective opinion

I would generally say keep the Activity as 'slim' as possible, holding a range of related fragments.

Thus, having multiple Activities is fine but they should each manage a set of (or a single) related fragments that are relevant for the current Activity.

Community
  • 1
  • 1
cYrixmorten
  • 7,110
  • 3
  • 25
  • 33
  • This looks great, thanks for answering. I'll definitely give that a shot. But I'm particularly looking for a solution that doesn't require third party libraries so I can better understand Android Design patterns before I save myself the hassle and use a third party Library. Do you happen to know a way to achieve this using Android standard libraries? – altyus Aug 04 '14 at 19:26
  • 1
    Have tried to come up with some suggestions. Also try to log the methods of your fragment, I have a feeling that onCreate might not be called when you think. Have a look here and scroll a bit down: http://developer.android.com/guide/components/fragments.html. – cYrixmorten Aug 04 '14 at 19:53
0

It just occurred to me to check out one of the Android Studio templates that Google Provides that I often overlooked. From Google's own templates, it appears clear that the preferred method for Master Detail Activities/Fragments is to have each Fragment Managed by their own activities (as I was attempting to achieve above).

(I should note that I was able to successfully achieve to flow that I wanted using a single Activitiy with multiple fragments and customizing the Animation and forcefully showing and hiding the up button.)

PersonListActivity.java

 public class PersonListActivity extends Activity
        implements PersonListFragment.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);
        setContentView(R.layout.activity_person_list);

        if (findViewById(R.id.person_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.
            ((PersonListFragment) getFragmentManager()
                    .findFragmentById(R.id.person_list))
                    .setActivateOnItemClick(true);
        }

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

    /**
     * Callback method from {@link PersonListFragment.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(PersonDetailFragment.ARG_ITEM_ID, id);
            PersonDetailFragment fragment = new PersonDetailFragment();
            fragment.setArguments(arguments);
            getFragmentManager().beginTransaction()
                    .replace(R.id.person_detail_container, fragment)
                    .commit();

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

PersonListFragment.java

  public class PersonListFragment 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 PersonListFragment() {
    }

    @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;
    }
}

PersonDetailActivity.java

    public class PersonDetailActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_person_detail);

        // Show the Up button in the action bar.
        getActionBar().setDisplayHomeAsUpEnabled(true);

        // savedInstanceState is non-null when there is fragment state
        // saved from previous configurations of this activity
        // (e.g. when rotating the screen from portrait to landscape).
        // In this case, the fragment will automatically be re-added
        // to its container so we don't need to manually add it.
        // For more information, see the Fragments API guide at:
        //
        // http://developer.android.com/guide/components/fragments.html
        //
        if (savedInstanceState == null) {
            // Create the detail fragment and add it to the activity
            // using a fragment transaction.
            Bundle arguments = new Bundle();
            arguments.putString(PersonDetailFragment.ARG_ITEM_ID,
                    getIntent().getStringExtra(PersonDetailFragment.ARG_ITEM_ID));
            PersonDetailFragment fragment = new PersonDetailFragment();
            fragment.setArguments(arguments);
            getFragmentManager().beginTransaction()
                    .add(R.id.person_detail_container, fragment)
                    .commit();
        }
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();
        if (id == android.R.id.home) {
            // This ID represents the Home or Up button. In the case of this
            // activity, the Up button is shown. For
            // more details, see the Navigation pattern on Android Design:
            //
            // http://developer.android.com/design/patterns/navigation.html#up-vs-back
            //
            navigateUpTo(new Intent(this, PersonListActivity.class));
            return true;
        }
        return super.onOptionsItemSelected(item);
    }
}

PersonDetailFragment.java

    public class PersonDetailFragment extends Fragment {
    /**
     * The fragment argument representing the item ID that this fragment
     * represents.
     */
    public static final String ARG_ITEM_ID = "item_id";

    /**
     * The dummy content this fragment is presenting.
     */
    private DummyContent.DummyItem mItem;

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

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

        if (getArguments().containsKey(ARG_ITEM_ID)) {
            // Load the dummy content specified by the fragment
            // arguments. In a real-world scenario, use a Loader
            // to load content from a content provider.
            mItem = DummyContent.ITEM_MAP.get(getArguments().getString(ARG_ITEM_ID));
        }
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        View rootView = inflater.inflate(R.layout.fragment_person_detail, container, false);

        // Show the dummy content as text in a TextView.
        if (mItem != null) {
            ((TextView) rootView.findViewById(R.id.person_detail)).setText(mItem.content);
        }

        return rootView;
    }
}
altyus
  • 606
  • 4
  • 13