17

I have an app with master/detail layout (1 activity, 1 ListView fragment and 1 detail fragment). When the user clicks an item in the ListView, a fragment transaction instantiates a detail fragment on the right-pane that includes the information corresponding to that item. When the detail fragment is shown I hide the initial action bar buttons/items and show 3 new AB items (done/delete/cancel). The user can clean the right-pane and return to the initial UI state by either pressing the back button or by pressing one of the 3 AB items.

The issue I'm experiencing is that when the user selects the app's home icon (i.e. "up navigation") the activity gets re-loaded (i.e. the animation that indicates that the activity is starting can be seen as both the action bar and the UI is been redrawn). The issue only happens when the app home icon is pressed. If the user presses the back button or a cancel/done/delete action bar button, the fragment is simply remove from the right-pane and the UI returns to initial state without any "re-loading".

The XML layout for the activity is the following (inside LinearLayout; prettify is hiding that line):

<fragment class="*.*.*.ListFragment"
        android:id="@+id/titles" android:layout_weight="1"
        android:layout_width="0px"
        android:layout_height="match_parent" />

<FrameLayout android:id="@+id/details" android:layout_weight="2"
        android:layout_width="0px"
        android:layout_height="match_parent" />

The DetailsFragement has the actionBar.setDisplayHomeAsUpEnabled statement in its onCreate method:

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


    ActionBar actionBar = getSherlockActivity().getSupportActionBar();
    actionBar.setDisplayHomeAsUpEnabled(true);

}

For both the ListView fragment and the Detail fragments the onCreateOptionsMenu() and onOptionsItemSelected() method are implemented within the fragments. Below the code for the Details fragment:

@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    inflater.inflate(R.menu.edit_menu, menu);
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {

    // some variable statements...

    switch (item.getItemId()) {
        case android.R.id.home:
            //Toast.makeText(getSherlockActivity(), "Tapped home", Toast.LENGTH_SHORT).show();
            onHomeSelectedListener.onHomeSelected();
            return true;

        case R.id.menu_edit_item_done:
            editedTask.setTitle(editedTaskTitle);
            onTaskEditedListener.onTaskEdited(editedTask, UPDATE_TASK, true);
            return true;

        default:
            return super.onOptionsItemSelected(item);

    }
}

In the host activity I implement the onHomeSelectedListner to handle the app home icon press (i.e. "up navigation":

public void onHomeSelected(){

    FragmentManager manager = getSupportFragmentManager();
    FragmentTransaction ft = manager.beginTransaction();
    TaskFragment taskFragment = (TaskFragment)getSupportFragmentManager().findFragmentById(R.id.details);
    ft.remove(taskFragment);
    ft.commit();
    manager.popBackStack();

}

The activity's listener in charged of handling all other action bar buttons (i.e. done/delete/cancel) is onTaskEditedListener and, aside of other code that processes some data, it has the same fragment transactions shown above.

Update(1/24) Based on tyczj and straya feedback I placed log statements inside onCreate(), onResume(), onPause() of the activity to determine the differences between onHomeSelected and onTaskEdited listeners. I'm able to confirm that during the "up navigation" event (i.e. onHomeSelected) onPause(), onCreate() and onResume() are called. Whereas during the onTaskEdited call (i.e. back button or done/delete/cancel press) none of those events are called.

Update (1/25) Based on a suggestion by Mark Murphy, I commented out the onHomeSelected method call in the "case android.R.id.home" statement just to see what would the Activity do. The thinking was that the app would do nothing since the are no statements. Turns out that is not the case. Even without a call to the listener method (i.e. that removes the fragment), the activity is restarted and the detail fragment is removed from the fragment container.

Update (2/28) I temporarily workaround the fact that my main activity was getting restarted by disabling the window animations (as highlighted in my own answer). However, through further testing I uncovered a bug. Thanks to Wolfram Rittmeyer's sample code I was able to figure out the real reason(s) why my activity was restarting (in master/detail single layout) during up navigation: 1) Although I was using this "onHomeSelectedListener" to properly remove the fragment from the backstack, I still had some remnant code in the ListView fragment's onOptionsItemSelected that was creating a new intent to start the hosting activity. That's why pressing the app's home icon was re-starting the activity. 2) In my final implementation (shown in my own answer), I got rid of the onHomeSelectedListener in the activity and replace the startActivity intent (i.e. offending code) inside the ListView's onOptionsItemSelected to use the fragment removal + popBackStack code originally in the onHomeSelectedListener.

cavega
  • 485
  • 3
  • 11
  • 23
  • It may be worth noting that the Tasks app uses a single Activity and shows/hides fragments for master/detail configuration, as opposed to having multiple activities. Check [this G+ post](https://plus.google.com/118292708268361843293/posts/EPf2UyyqD8n) and the comments. – curioustechizen Feb 27 '13 at 05:16
  • @curioustechizen In master/detail configuration I'm also using a single activity. I've read that G+ post before. Although it didn't discussed the topic of up navigation (as pertaining to this question) it served as a reminder to tackled the problem of "rotational stability", so thanks for sharing! – cavega Feb 28 '13 at 02:04

4 Answers4

8

After much research and poking around, turns out that only reason why my activity was restarting during "up navigation" for master/detail configuration was because I left some code in the ListView Fragment's onOptionsItemSelected that was creating an intent to start the main activity in addition to my full fragment transaction code elsewhere. Below is the final implementation with which I got "up navigation" to work properly on both phone (multiple activities) and tablet (single activity/multi-pane) configurations. Thanks to Wolfram Rittmeyer for a couple of hints in his code (link in the comment section) that help me pinpoint my problem!

Main Activity: Hosts the fragments and performs some other app-specific operations

ListView Fragment: Handles "up navigation in table configuration

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {
        case android.R.id.home:
            if(mDualPane){
                FragmentManager manager = getSherlockActivity().getSupportFragmentManager();
                FragmentTransaction ft = manager.beginTransaction();
                DetailFragment detailFragment = (DetailFragment)manager.findFragmentById(R.id.details);
                ft.remove(detailFragment);
                ft.commit();
                manager.popBackStack();
                getSherlockActivity().getSupportActionBar().setDisplayHomeAsUpEnabled(false);
                getSherlockActivity().getSupportActionBar().setHomeButtonEnabled(false);
            }
            return true;

        // Other case statements...

        default:
            return super.onOptionsItemSelected(item);
    }
}

Details Fragment: Handles up navigation in phone configuration

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

    // Sets "up navigation" for both phone/tablet configurations
    ActionBar actionBar = getSherlockActivity().getSupportActionBar();
    actionBar.setDisplayHomeAsUpEnabled(true);
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {

    switch (item.getItemId()) {
        case android.R.id.home:
            if(!mDualPane){
                Intent parentActivityIntent = new Intent(getSherlockActivity(), MainActivity.class);
                parentActivityIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
                startActivity(parentActivityIntent);
                getSherlockActivity().finish();
            }
            return true;

        // Other case statements...

        default:
            return super.onOptionsItemSelected(item);
    }

}
Community
  • 1
  • 1
cavega
  • 485
  • 3
  • 11
  • 23
  • 1
    Moderators, for future reference, please add the link of cited stackoverflow answer to my response. – cavega Feb 27 '13 at 02:41
  • 1
    Sorry that I'm so late. What do you think about this one: https://gist.github.com/writtmeyer/5052512 – Wolfram Rittmeyer Feb 28 '13 at 09:32
  • +1 for Useful info! anybody knows how to use [Master detail and navigation drawer](http://stackoverflow.com/questions/20290973/navigation-drawer-and-master-detail-flow) together? Master detail works when we are using fragment in xml with API 11 with Layout Aliases, I'm looking for how to implement the same in navigation drawer where we are loading the fragment programmatically! – LOG_TAG Aug 07 '14 at 04:40
  • 1
    @LOG_TAG I think the only app I've seen that does Drawer + Master/Detail is the Play Store. You probably want to give a shot at using a Fragment to host the master/detail layout and ChildFragments for the individual Master and Detail fragments. I haven't try this myself but it might be worth a shot. – cavega Aug 18 '14 at 21:19
  • @cavega Thanks for the info! Let you know the results! – LOG_TAG Aug 19 '14 at 15:05
3

If you look at the Navigation Design Pattern you will see that you want to return to the starting activity when the home button is hit.

So say you have 2 Activities call them A1 and A2. Clicking on something in A1 takes you to A2. If the user hits the home button you should return them to A1 clearing the stack of everything up until that activity like this

Intent intent = new Intent(this, A1.class);  
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);

this is what the flag Intent.FLAG_ACTIVITY_CLEAR_TOP does

If set, and the activity being launched is already running in the current task, then instead of launching a new instance of that activity, all of the other activities on top of it will be closed and this Intent will be delivered to the (now on top) old activity as a new Intent.

For example, consider a task consisting of the activities: A, B, C, D. If D calls startActivity() with an Intent that resolves to the component of activity B, then C and D will be finished and B receive the given Intent, resulting in the stack now being: A, B.`

The currently running instance of activity B in the above example will either receive the new intent you are starting here in its onNewIntent() method, or be itself finished and restarted with the new intent. If it has declared its launch mode to be "multiple" (the default) and you have not set FLAG_ACTIVITY_SINGLE_TOP in the same intent, then it will be finished and re-created; for all other launch modes or if FLAG_ACTIVITY_SINGLE_TOP is set then this Intent will be delivered to the current instance's onNewIntent().

This launch mode can also be used to good effect in conjunction with FLAG_ACTIVITY_NEW_TASK: if used to start the root activity of a task, it will bring any currently running instance of that task to the foreground, and then clear it to its root state. This is especially useful, for example, when launching an activity from the notification manager.

Community
  • 1
  • 1
tyczj
  • 71,600
  • 54
  • 194
  • 296
  • I'm doing what you suggested in the phone configuration where there are 2 activities. However, on a tablet configuration both the ListView and Details fragments are hosted by the same activity (i.e. Details fragment was started from fragment transaction in ListView fragment). Why would I need an intent if I'm already in the home activity? Pressing the home/up affordance button should simply remove the fragment from the stack. My code is removing the fragment properly, the problem is that the main and only activity seems to be re-started because of the fly-in animation I see. – cavega Jan 23 '13 at 05:08
  • then why dont you just pop the backstack until you get to where you want to go. When you start your fragment and use `addToBackStack("fragmentname")` instead of `addToBackStack(null)` then when the home button is hit you use `.popBackStack("name of fragment you wan to go back to", FragmentManager.POP_BACK_STACK_INCLUSIVE)` – tyczj Jan 23 '13 at 14:24
  • If you look at my onHomeSelected method I'm already doing popBackStack. The detail fragment is on the right-pane of the activity's layout (listview fragment is on the left-pane). When the user is done viewing an item from the list the right-pane is supposed to be empty so there is no fragment to navigate to. The only problem with my code is that "up navigation" as processed by "case android.R.id.home:" and onHomeSelected is showing an animation as if the whole activity (including action bar and 2 fragments) is getting restarted and does not work the same way as onTaskEdited. – cavega Jan 23 '13 at 15:14
  • well I dont know then, with the amount of code given the best I can say then is put some breakpoints in and start walking through the code line by line. try just popping the backstack without removing the fragment and see if that changes anything – tyczj Jan 23 '13 at 15:32
  • Thanks for the suggestions. I've re-worded the problem description and added extra pieces of code. Let me know if additional information is needed. – cavega Jan 24 '13 at 01:54
0

don't: break and then return super.onOptionsItemSelected(item), rather just: return true;

UPDATE:

So you're saying the Activity is "restarted" based on what you see happen with Views, but can you confirm what may or may not happen to the Activity (and Fragments for that matter) by using logging in the various lifecycle methods? That way you can be sure of what the current (erroneous) behaviour is before moving forward with diagnosis.

UPDATE:

OK, good to be sure about behaviour :) Now regarding your question "What is the correct way to implement "up navigation" for a master/detail layout (1 activity/2fragments)? ": The typical way is that the 2 Fragments got added within a single FragmentTransaction and you simply popBackStack to remove them and go back to whatever previous state was. I think you're doubling up by manually removing a Fragment within a FragmentTransaction and then popping backstack. Try just popBackStack. Oh and just to be sure and consistent, since you're using ActionBarSherlock and support.v4 are you using a FragmentActivity (rather than an Activity) and SherlockFragment?

straya
  • 5,002
  • 1
  • 28
  • 35
  • I changed the break statements to "return true" but even then, based on this article from [Google's training](http://developer.android.com/guide/topics/ui/actionbar.html) using the super implementation as the default is required. Even after those modifications (code updated in original question) I still experience the fly-in animation in the tablet configuration. Please keep in mind that both onHomeSelectedListener and onTaskEditedListener use the same fragment transaction and only the "home" switch case statement is resulting in an activity restart. – cavega Jan 23 '13 at 05:27
  • I used Log statements and was able to confirm that onPause(), onCreate(), and onResume() are called only during up navigation and hence, the activity is getting re-created. This does not happen when the user presses the back button or any of the Action bar items to navigate out of the fragment. – cavega Jan 24 '13 at 01:57
  • All the master/detail Google examples (including the ADT template) instantiate the "listview fragment" within the xml layout. The details (right-pane) fragment is the only one that requires fragment transactions through the FrameLayout defined in the xml container. This is exactly what I'm doing in my code. I'll give it a try to your suggestion. However, I'm using the same fragment transactions to remove the fragment in both 1) onHomeSelected and 2) onTaskEdited, so this doesn't explain why the Activity only gets restarted for onHomeSelected (up navigation). – cavega Jan 24 '13 at 19:59
0

I think you should handle the Up button only inside the activity. If youre in a phone, the up button will be handled by activity that acts as a wrapper of that fragment, in tablet (master/detail pattern) you dont want it anyways

urSus
  • 12,492
  • 12
  • 69
  • 89
  • I would agree with you, but even Google's own Gmail app (tablet configutation) allows the user to use "up nagivation" to switch between fragments in the same main activity. If you look closely at the Gmail app, you don't see any animation indicating that the "home activity" is getting re-started. – cavega Feb 21 '13 at 02:05
  • That means that when the Gmail app switches back and forth between State#1: folder(left-pane)/email list (right-pane) and State#2: email list(left-pane)/selected email (right-pane), all is done within the same activity. So either 1) Google is actually using 2 activities (and did a hack to disable the "activity restarted" animation or 2) they are using a single activity. – cavega Feb 21 '13 at 02:05
  • Another app called "Tasks" is also able to use "up nagivation" in a master/detail configuration without the activity getting re-started. Unless, like my guess above, they'be disabled the animation that indicates when an activity is getting re-started. – cavega Feb 21 '13 at 02:07
  • well, i dont have a tablet, just emulator, but what I meant was to have the activity wrapping the fragment in a phone layout handle the up button, and when youre in a tablet, this activity doesnt exist, so therefore theres not up button to be handled, which would be correct in this case of simple master/detail. – urSus Feb 21 '13 at 02:13