Android Dual Pane - Going From Details To List - android

I have followed the official documentation http://developer.android.com/guide/practices/tablets-and-handsets.html for Tablet support to create dual pane layout that works as shown below, in that in small screens (phones) it uses one Fragment inside one Activity to display a list of objects and another fragment inside another Activity.
Every other documentation I read talks about a one way flow from Master to details, now I want to go back the other way, from details to master and I am stuck.
In the details, I have added an Item that I want to display in the list and I want this to be dynamic such that I can add few items and each time I hit save I want the List to grow.
This is what I have done so far
In FragmentA(List Fragment) I have a method that (re)loads the data and call notifyDataSetChanged on the adapter.
I added a method in the call back that is called each time an item is added. And both Activity A and Activity B implements this listener
So when I add an item in FragmentB(Details Fragment) I call the listener and on Activity A which is housing the dual pane layout I try this
public void OnNewCustomerAdded() {
Fragment frag = null;
frag = getSupportFragmentManager().findFragmentByTag("CustomertListFragment");
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.detach(frag);
ft.attach(frag);
ft.commit();
}
Unfortunately that throws a NPE, and also if I call the methods directly in the Fragment to reload data, that throws an NPE. The only thing that works with some side effects is this
public void OnNewClientAdded() {
Intent intent = getIntent();
finish();
startActivity(intent);
}
So how can I safely restart a Fragment inside an Activity without restarting the other Fragment.

Wow, this is quite tricky, never expected it to be this challenging. Well this is how I solve it.
First I removed the second activity and reworked the code to show single pane in handheld devices and dual pane in tablets using just one Activity instead of two. This is not necessary but it helps when you are dealing with one set of lifecycles and listeners.
Then to actually have items added in the DetailsFragment appear immediately in the ListFragment while still having the Details Fragment open. I finished the containing Activity, restarted it and passed it an intent that tells it to start up the DetailsFragment
Remember that the ListFragment is set to start up no matter which device size. So you just need to start the DetailsFragment and since there is already a call back that does that it makes it easy so here is the code
//Callback method for when an item is added, called from the Details
//Fragment
public void OnNewCustomerAdded() {
Intent mIntent = getIntent();
mIntent.putExtra(Constants.SHOULD_START_CUSTOMER_DETAILS, true);
finish();
startActivity(mIntent);
}
Then in the onCreate of the Activity, after the ListFragment has been started, you do this
boolean shouldStartCustomerDetails = getIntent().getBooleanExtra(Constants.SHOULD_START_CLIENT_DETAILS, false);
if (shouldStartClientDetails){
OnCustomerListItemSelected(0);
}
The OnCustomertListItemSelected is the standard mCallback listener that you get if you created a Master/Details Activity in Android Studio, I just modified it to suit my app like so
/**
* Callback method from {#link OnCustomerListItemSelectedListener}
* indicating that the item with the given ID was selected.
*/
#Override
public void OnCustomerListItemSelected(long 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.
CustomerDetailsFragment fragment = CustomerDetailsFragment.newInstance(id);
getSupportFragmentManager().beginTransaction()
.replace(R.id.customeractivity_detail_container, fragment)
.commit();
} else {
// In single-pane mode, simply start the detail activity
// for the selected item ID.
CustomerDetailsFragment fragment =
CustomerDetailsFragment.newInstance(getIntent().getLongExtra(Constants.ARG_ITEM_ID, 0));
getSupportFragmentManager().beginTransaction()
.add(R.id.customeractivity_list_container, fragment)
.commit();
}
}

Related

Creating new Fragment vs Refreshing existing one - what is better for performance?

I try to implement default design patterns for tablets and handsets:
But I don't clearly understand should I recreate Fragment B every listView.setOnItemClickListener call or I need just to update the views of fragment B? What is better for performance and why?
I think creating new Fragment (Activity) instead of refreshing the existing one gives more advantages:
More simple code.
Ability to use normal activity stack to handle back-navigation.
But why official android documentation uses updating fragment's content in their explanation?
public class MainActivity extends Activity implements TitlesFragment.OnItemSelectedListener {
...
/** This is a callback that the list fragment (Fragment A)
calls when a list item is selected */
public void onItemSelected(int position) {
DisplayFragment displayFrag = (DisplayFragment) getFragmentManager()
.findFragmentById(R.id.display_frag);
if (displayFrag == null) {
// DisplayFragment (Fragment B) is not in the layout (handset layout),
// so start DisplayActivity (Activity B)
// and pass it the info about the selected item
Intent intent = new Intent(this, DisplayActivity.class);
intent.putExtra("position", position);
startActivity(intent);
} else {
// DisplayFragment (Fragment B) is in the layout (tablet layout),
// so tell the fragment to update
displayFrag.updateContent(position); //WHY?
}
}
}
In documentation the second approach is used: on a tablet - multiple fragments in one activity; on a handset - separate activities to host each fragment. By searching displayFrag you actually check if it's a two-pane mode.
If there is no fragment then you have to start a new activity, otherwise update content.
Creating a new fragment can simplify code but also stress a garbage collector. It also varies depending on specific update, you are not only creating new objects but may rebuild full view hierarchy.
Smart reusing always gives a better performance (think about viewholder pattern) sometimes in sacrifice of readability.

Showing and hiding fragments are not committed (don't occur) immediately when SwipeRefreshLayout is refreshing

So basically what I'm working on is very similar to Instagram application, where there're a number of tabs and users can switch to any tab without any delay no matter what there's anything going on, such as refreshing, reloading, and etc. It also uses back button to go back to the previous preserved tab.
In order to achieve this, I've used FragmentManager with FragmentTransaction to show and hide each fragment which represents each tab. I didn't use replace or attach / detach because they destroy view hierarchy of previous tab.
My implementation works pretty well except that showing and hiding fragments are not committed (I highly doubt that this is a right word to say but so far that's how I understood the flow.), or don't occur immediately when SwipeRefreshLayout is refreshing on the fragment (to be hidden) which was added to FragmentManager later than the one to show.
My implementation follows the rules like these. Let's say we have 4 tabs and my MainActivity is showing the first tab, say FirstFragment, and the user selects the second tab, SecondFragment. Because SecondFragment had never been added before, I add it to FragmentManager by using FragmentTransaction.add and hide FirstFragment by using FragmentTransaction.hide. If the user selects the first tab again, because FirstFragment was previously added to FragmentManager, it doesn't add but only show FirstFragment and just hide SecondFragment. And selecting between these two tabs works smoothly.
But when the user "refreshes" SecondFragment's SwipeRefreshLayout and selects the first tab, FragmentTransaction waits for SecondFragment's refresh to be finished and commits(?) the actual transaction. The strange thing is that the transaction is committed immediately the other way around, from FirstFragment's refresh to SecondFragment.
Because this occurs by the order of addition to FragmentManager, I doubt that the order of addition somehow affects backstack of fragments and there might exists something like UI thread priority so that it forces the fragment transaction to be taken place after later-added fragment's UI transition finishes. But I just don't have enough clues to solve the issue. I've tried attach / detach and backstack thing on FragmentTransaction but couldn't solve the issue. I've tried both FragmentTransaction.commit and FragmentTransaction.commitAllowingStateLoss but neither solved the issue.
These are my MainActivity's sample code.
private ArrayList<Integer> mFragmentsStack; // This is simple psuedo-stack which only stores
// the order of fragments stack to collaborate
// when back button is pressed.
private ArrayList<Fragment> mFragmentsList;
#Override
protected void onCreate() {
mFragmentsStack = new ArrayList<>();
mFragmentsList = new ArrayList<>();
mFragmentsList.add(FirstFragment.newInstance());
mFragmentsList.add(SecondFragment.newInstance());
mFragmentsList.add(ThirdFragment.newInstance());
mFragmentsList.add(FourthFragment.newInstance());
mMainTab = (MainTab) findViewById(R.id.main_tab);
mMainTab.setOnMainTabClickListener(this);
int currentTab = DEFAULT_TAB;
mFragmentsStack.add(currentTab);
getSupportFragmentManager().beginTransaction().add(R.id.main_frame_layout,
mFragmentsList.get(currentTab), String.valueOf(currentTab)).commit();
mMainTab.setCurrentTab(currentTab);
}
// This is custom interface.
#Override
public void onTabClick(int oldPosition, int newPosition) {
if (oldPosition != newPosition) {
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
// First hide the old tab.
fragmentTransaction.hide(mFragmentsList.get(oldPosition));
// Recalculate the fragment stack.
if (mFragmentsStack.contains(newPosition)) {
mFragmentsStack.remove((Integer) newPosition);
}
mFragmentsStack.add(newPosition);
// Add new fragment if it's not added before, or show new fragment which was already hidden.
Fragment fragment = getSupportFragmentManager().findFragmentByTag(String.valueOf(newPosition));
if (fragment != null) {
fragmentTransaction.show(fragment);
} else {
fragmentTransaction.add(R.id.main_frame_layout, mFragmentsList.get(newPosition),
String.valueOf(newPosition));
}
// Commit the transaction.
fragmentTransaction.commitAllowingStateLoss();
}
}
// It mimics the tab behavior of Instagram Android application.
#Override
public void onBackPressed() {
// If there's only one fragment on stack, super.onBackPressed.
// If it's not, then hide the current fragment and show the previous fragment.
int lastIndexOfFragmentsStack = mFragmentsStack.size() - 1;
if (lastIndexOfFragmentsStack - 1 >= 0) {
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.hide(mFragmentsList.get(mFragmentsStack.get(lastIndexOfFragmentsStack)));
fragmentTransaction.show(mFragmentsList.get(mFragmentsStack.get(lastIndexOfFragmentsStack - 1)));
fragmentTransaction.commitAllowingStateLoss();
mMainTab.setCurrentTab(mFragmentsStack.get(lastIndexOfFragmentsStack - 1));
mFragmentsStack.remove(lastIndexOfFragmentsStack);
} else {
super.onBackPressed();
}
}
Just faced the same issue with only difference - I'm switching fragments on toolbar buttons click.
I've managed to get rid of overlapping fragments overriding onHiddenChanged:
#Override
public void onHiddenChanged(boolean hidden) {
super.onHiddenChanged(hidden);
if (hidden) {
yourSwipeRefreshLayout.setRefreshing(false);
}
}

Activity recreates Fragments on Orientation Change because reference is lost, how to avoid that?

I'm trying to learn Android Fragments and I have a very specific problem concerning Fragment management, because screen orientation screws up my implementation.
EDIT: Already solved my problem, see the "Update" below.
Short version:
Using static Fragments, if I change screen orientation, the reference to R.id.fragment is lost and the Activity re-creates the Fragment causing problems because another Fragment is still present on the Layout (because they're defined on the XML maybe).
Context:
I have a Master/Detail workflow using the default Eclipse template and I have a different type of Fragment for every tab on the ItemList. Ideally, what I want to do is switch between fragments, but I want to retain their current state without using the BackStack, since I want to navigate with the ItemList, and using the Back button to close the App.
I couldn't find any solutions for this specific problem and I tried with a lot of different approaches. Right now I'm using static fragments defined in the main Layout:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/item_detail_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ItemDetailActivity"
tools:ignore="MergeRootFrame" >
<fragment android:name="com.example.pintproject.DevicesFragment"
android:id="#+id/devices"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="match_parent"
/>
<fragment android:name="com.example.pintproject.ItemDetailFragment"
android:id="#+id/detail"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="match_parent"
/>
</FrameLayout>
In the ItemListActivity onCreate(), I look for the Fragments in the layout, and add them if they aren't created yet, and I hold a reference to the current active Detail Fragment so I can hide it / show the fragment I switch to.
I'm using hide/show instead of replace because replace destroys the Fragment:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_item_list);
if (findViewById(R.id.item_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.
((ItemListFragment) getSupportFragmentManager().findFragmentById(
R.id.item_list)).setActivateOnItemClick(true);
}
df = (DevicesFragment) getSupportFragmentManager().findFragmentById(R.id.devices);
if (df==null){
df = new DevicesFragment();
getSupportFragmentManager().beginTransaction().add(R.id.item_detail_container,df).commit();
getSupportFragmentManager().beginTransaction().hide(df).commit();
}
idf = (ItemDetailFragment) getSupportFragmentManager().findFragmentById(R.id.detail);
if (idf==null){
idf = new ItemDetailFragment();
getSupportFragmentManager().beginTransaction().add(R.id.item_detail_container,idf).commit();
getSupportFragmentManager().beginTransaction().hide(idf).commit();
}
mContent = df;
#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.
switch (Integer.valueOf(id)){
case 1:{
if (idf!=null){
getSupportFragmentManager().beginTransaction().hide(mContent).commit();
getSupportFragmentManager().beginTransaction().show(idf).commit();
mContent = idf;
}
}break;
case 2:{
if (df!=null){
getSupportFragmentManager().beginTransaction().hide(mContent).commit();
getSupportFragmentManager().beginTransaction().show(df).commit();
mContent = df;
}
}break;
}
} else {
// In single-pane mode, simply start the detail activity
// for the selected item ID.
Intent detailIntent = new Intent(this, ItemDetailActivity.class);
detailIntent.putExtra(ItemDetailFragment.ARG_ITEM_ID, id);
startActivity(detailIntent);
}
}
Problem:
With this approach, the Fragments hide/show without any problems and hold the status, but if I make an Orientation Change, they are destroyed and recreated again.
I know they are destroyed because I'm not using setRetainInstance(), but the problem is when I change orientation, the Activity loses the reference to the Fragment, and
df = (DevicesFragment) getSupportFragmentManager().findFragmentById(R.id.devices);
is null, so the program creates another Fragment. If I change the orientation again, not only the program re-creates two new Fragments, but two more Fragments are somehow added to the layout and they aren't even hidden, they are shown one above another.
If I use setRetainInstance(), the Fragment holds the state when Orientation is changed, but still, the activity reference to the Fragment is null, and creates a new Fragment above the existing one, having two of each Fragment.
Example:
I create Fragment A and Fragment B in Landscape orientation. Both work fine and I can switch between them.
I change orientation to Portrait, Fragment A and Fragment B are destroyed and a new Fragment A' and Fragment B' are created, still, they work fine.
I change orientation again to Landscape, Fragment A' and Fragment B' are destroyed, a new Fragment A'' and Fragment B'' are created, but the screen shows another Fragment A and Fragment B, both at the same time (one above another, let's call them residual), and these new A'' and B'' work fine but are shown above residual A and B.
From this point on, every time I change orientation, 2 new Fragments are added to the previous ones, but they don't even hold the previous state.
I hope the example is clear enough. I think the problem is the Activity not holding view references when the orientation is changed, creates them again and I don't really know how to work around that.
UPDATE:
I solved my problem by using findFragmentByTag instead of findFragmentById. Since I can now retrieve Fragment s already created, I have to add them to the container adding a specific tag to search for.
So my test code looks like this:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_item_list);
if (findViewById(R.id.item_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.
((ItemListFragment) getSupportFragmentManager().findFragmentById(
R.id.item_list)).setActivateOnItemClick(true);
}
df = (DevicesFragment) getSupportFragmentManager().findFragmentByTag("df");
idf = (ItemDetailFragment) getSupportFragmentManager().findFragmentByTag("idf");
if (savedInstanceState==null){
if (df==null){
df = new DevicesFragment();
getSupportFragmentManager().beginTransaction().add(R.id.item_detail_container,df, "df").commit();
getSupportFragmentManager().beginTransaction().hide(df).commit();
}
if (idf==null){
idf = new ItemDetailFragment();
getSupportFragmentManager().beginTransaction().add(R.id.item_detail_container,idf,"idf").commit();
getSupportFragmentManager().beginTransaction().hide(idf).commit();
}
} else {
Log.i("OUT","INSTANCE NOT NULL");
}
mContent = df;
}
This is fully functional, also have to setRetainInstance(true) for every Fragment and they hold their current state no matter how many times we change the orientation.
You must never hold a reference to the fragment. Instead. Whenever you need something from it, retrieve the reference for a short moment.
public ItemListFragment getItemListFragment() {
return ((ItemListFragment) getSupportFragmentManager().findFragmentById(
R.id.item_list));
}
Then, whenever you need to get data from it, use
final ItemListFragment listFragment = getItemListFragment();
if (listFragment != null) {
// do something
}
And avoid calling setters. You can define the setters, but it's a better practice to either pass an arguments when creating a Fragment or retrieve the data by getActivity() from the Fragment itself, as described below.
This is done because the Fragment lifecycle not always matches the Activity one.
If you ever have to call setter from Activity, don't forget to save the value in Fragment's onSaveInstanceState(), if needed.
So instead of calling
setActivateOnItemClick(true);
From Activity, do it from the Fragment.
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
final YourActivity activity = (Activtity) getYourActivity();
setActivateOnItemClick(activity.isMultiPane());
}
This way when the Fragment is re-created after Activity onCreate() (only in which you handle the value setting), it will has always access to the value
And define isMultiPane method from Activity, of course
public boolean isMultiPane() {
return mTwoPane;
}
Since there are no answer yet, here is my opinion :
When your orientation changes, your fragment is being recreated and you loose your data, right ? I think this is exactly whete the "savedInstanceState" is made for :
Caution: Your activity will be destroyed and recreated each time the user rotates the screen. When the screen changes orientation, the system destroys and recreates the foreground activity because the screen configuration has changed and your activity might need to load alternative resources (such as the layout).
Here is a link that can explain you how to handle that recreation
Hope this is useful to you ! =)

after configuration change fragment from backstack is now sharing the FrameLayout?

Problems with app:
When orientation changes the app is experiencing these problems:
Both FragmentA and FragmentC now occupy the FrameLayout container.
What works: Everything works as I want it to...prior to rotating the screen.
Activity description in brief:
EditActivity Purpose: edit collection and item fields.
Fragments this activity programmatically creates:
FragmentA - fragment for editing collection fields
FragmentB - ListFragment of items in collection
FragmentC - fragment for editing item fields.
Initial layout: FragmentA sits atop FragmentB, each in their own FrameLayouts.
When user clicks FragmentB's listview item: replace FragmentA with FragmentC to allow user to edit that item's fields. Now FragmentC sits atop FragmentB.
This seems like a very simple notion: the top portion of the activity is for editing either properties of the collection as a whole or a single item from the collection. I don't feel I have done anything wondrous with the layout so I'm a fair bit perplexed that a simple rotation of the phone (emulator) causes these problems that I am having such a dastardly time trying to fix.
Why the Android Fragment Guide example doesn't work for me: their example is much like what I am doing but their detail fragment is either being opened in a new activity or in its own Frame within the current activity, they don't do any swapping of fragments so I cannot glean how they would use the onSaveIstanceState to preserve the fragments that are visible and then use that information in onCreate to recreate the UI that was there prior to orientation change.
EDIT: took out one problem by caving and putting the listfragment in the XML, this solved the perpetual spinning "loading..." problem.
Solved. Oh, the rabbit holes I traveled... At any rate, if you run into problems like this a couple of things to consider:
ultimately I didn't have to write any code in onSaveInstanceState(Bundle outState).
Ultimately I didn't have to make any considerations about handling the backstack in onSaveInstanceState or deal with it the activity's onCreate.
When first "adding" fragments programmatically to the FrameLayout, use replace instead of `add' - this was likely one of the roots of my troubles.
in onCreate check if savedInstanceState's bundle is null, if(savedInstanceState == null), and if it is then I know that the activity hasn't been torn down previously by a configuration change, so here I build fragments that should be displayed right at activity start up. Other fragments that are programmatically brought to life elsewhere (ie, later than the activity's onCreate()), they don't belong in the if, they belong in the else:
else onSaveInstanceState != null and I know there's only one reason this thing's not null, because the system made a bundle named outState in onSaveInstanceState(Bundle outState) and hucked it at the activity's onCreate method where I can now get my grubbies on it. So it is here that I know a couple of things:
for sure the fragments I created in the activity's onCreate are still a part of the activity (I didn't detach or destroy them), but, I cannot make that same claim for the fragments brought to life via a user's actions, those fragments may or may not be currently (at the time of orientation aka configuration change) attached to the activity.
This is a good place for an if-this-thing-is-attached clause. One of things I initially messed up on was I failed to give ALL of my programmatically added fragments a tag; give all programmatically added fragments tags. I can then find out if the savedInstanceState bundle contains that key with savedInstanceState.containsKey(MY_FRAG_TAG) and with getFragmentManager().findFragmentByTag(MY_FRAG_TAG)
So here's the activity's onCreate (simplified):
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_edit);
// ...omitted code...
if(savedInstanceState == null){
// create fragment for collection edit buttons
editCollection = FragmentA.newInstance(someVariable);
// programmatically add fragment to ViewGroup
getFragmentManager().beginTransaction().replace(R.id.edit_topFrame, editCollection, EDIT_COLLECTIONS_TAG).commit();
}
// else there be stuff inside the savedInstanceState bundle
else{
// fragments that will always be in the savedInstanceState bundle
editCollectionFragment = (FragmentA)getFragmentManager().findFragmentByTag(EDIT_COLLECTIONS_TAG);
// fragments that may not be in the bundle
if(savedInstanceState.containsKey(EDIT_ITEM_TAG)){
editItemFragment = (FragmentC)getFragmentManager().getFragment(savedInstanceState, EDIT_ITEM_TAG);
}
}
// This fragment is NOT programmatically added, ie, it is statically found in an XML file.
// Hence, the system will take care of preserving this fragment on configuration changes.
listFrag = (ListViewFragment)getFragmentManager().findFragmentById(R.id.ListFragment);
// create adapter
adapter = new EditCursorAdapter(this, null);
// set list fragment adapter
listFrag.setListAdapter(adapter);
// prepare the loader
getLoaderManager().initLoader(LOADER_ID, null, this);
}
And the Activity's listener for the list fragment, where FragmentC is swapped for FragmentA:
// listfragment listener
#Override
public void listFragListener(Cursor cursor) {
// checking backstack size
Log.d(TAG, SCOPE +"backstack size: "+getFragmentManager().getBackStackEntryCount());
// With each listview click there should be only one item in the backstack.
getFragmentManager().popBackStack();
// create new fragment
editItemFragment = FragmentC.newInstance(cursor);
// programmatically add new fragment
FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.replace(R.id.edit_topFrame, editItemFragment, EDIT_ITEM_TAG);
ft.addToBackStack("pop all of these"); // was testing different ways of popping
ft.commit();
// interesting: this reports the same value as the first log in this method.
// ...clearly addToBackStack(null).commit() doesn't populate the backstack immediately?
Log.d(TAG, SCOPE +"backstack size: "+getFragmentManager().getBackStackEntryCount());
}
And onSaveInstanceState is naked as a jay bird:
#Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
}
Summary: I have the activity functioning exactly as I want it to.
Now, if I had a bunch of added fragments then I might handle them in a more programmatic fashion rather than by hard coding the if(savedInstanceState.contains(*hard coded key*). This I tested a little bit but cannot attest to its efficacy, however for someone out there this might spark an idea of what you can do:
Make a private Set of added fragments:
// Collection of Frag Tags
private Set<String> AddedFragmentTagsSet = new HashSet<String>();
In onAttachFragment do something like:
#Override
public void onAttachFragment(Fragment fragment) {
super.onAttachFragment(fragment);
// logging which fragments get attached and when
Log.d(TAG, SCOPE +"attached fragment: " +fragment.toString());
// NOTE: XML frags have not frigg'n tags
// add attached fragment's tag to set of tags for attached fragments
AddedFragmentTagsSet.add(fragment.getTag());
// if a fragment has become detached remove its tag from the set
for(String tag : AddedFragmentTagsSet){
if(getFragmentManager().findFragmentByTag(tag).isDetached()){
AddedFragmentTagsSet.remove(tag);
}
Log.d(TAG, SCOPE +"contents of AddedFragmentTagsSet: " +tag);
}
}
Then in the activity's onCreate and within savedInstanceState clauses:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_edit);
// ...omitted code...
if(savedInstanceState == null){
// create fragment for collection edit buttons
editCollection = FragmentA.newInstance(someVariable);
// programmatically add fragment to ViewGroup
getFragmentManager().beginTransaction().replace(R.id.edit_topFrame, editCollection, EDIT_COLLECTIONS_TAG).commit();
}
// else there be stuff inside the savedInstanceState bundle
else{
// fragments that will always be in the savedInstanceState bundle
editCollectionFragment = (FragmentA)getFragmentManager().findFragmentByTag(EDIT_COLLECTIONS_TAG);
//////////// find entries that are common to AddedFragmentTagsSet & savedInstanceState's set of keys ///////////
Set<String> commonKeys = savedInstanceState.keySet();
commonKeys.retainAll(AddedFragmentTagsSet);
for(String key : commonKeys){
editItemFragment = FragmentC)getFragmentManager().getFragment(savedInstanceState, key);
}
}
}
...but that is untested and presented merely to spark ideas; in trying to figure out what was wrong with my activity's handling of configuration changes I did stumble and fumble in this direction and think it might bear fruit for the right person; though ultimately, obviously, I found a simpler way to fix my issues this time around.

Single Activity with multiple fragments, typical scenario. Struggling to achieve desired behavior

I have an application running a single activity with multiple (2) fragments at a given time. I've got a fragment on the left which functions as a menu for which fragment to
display on the right hand side.
As an example lets say the menu consists of different sports; Football, Basketball, Baseball, Skiing, etc. When the user selects a sport, a fragment with details on the specific sport is displayed to the right.
I've set up my app to display two fragments at once in layout-large and layout-small-landscape. In layout-small-portrait however, only one fragment is displayed at a given time.
Imagine this; a user is browsing the app in layout-small-landscape (two fragments at a time) and selects a sport, Football. Shortly after he selects Basketball. If he now chooses to rotate into layout-small-portrait (one fragment at a time) I want the following to happen:
The Basketball fragment should be visible, but if he presses the back button, he should return to the menu and not to the Football fragment (!) which by default is the previous fragment in the back stack.
I have currently solved this like the following:
....
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// static fragments
if(menuFragment == null) menuFragment = new MenuFragment();
if(baseFragment == null) baseFragment = new TimerFragment(); // default content fragment
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
// Determine what layout we're in..
if(app().getLayoutBehavior(this) == LayoutBehavior.SINGLE_FRAGMENT) {
// We are currently in single fragment mode
if(savedInstanceState != null) {
if(!rotateFromSingleToDual) {
// We just changed orientation from dual fragments to single fragment!
// Clear the entire fragment back stack
for(int i=0;i<getSupportFragmentManager().getBackStackEntryCount();i++) {
getSupportFragmentManager().popBackStack();
}
ft.replace(R.id.fragmentOne, menuFragment); // Add menu fragment at the bottom of the stack
ft.replace(R.id.fragmentOne, baseFragment);
ft.addToBackStack(null);
ft.commit();
}
rotateFromSingleToDual = true;
return;
}
rotateFromSingleToDual = true;
ft.replace(R.id.fragmentOne, menuFragment);
} else if(app().getLayoutBehavior(this) == LayoutBehavior.DUAL_FRAGMENTS) {
// We are now in dual fragments mode
if(savedInstanceState != null) {
if(rotateFromSingleToDual) {
// We just changed orientation from single fragment to dual fragments!
ft.replace(R.id.fragmentOne, menuFragment);
ft.replace(R.id.fragmentTwo, baseFragment);
ft.commit();
}
rotateFromSingleToDual = false;
return;
}
rotateFromSingleToDual = false;
ft.replace(R.id.fragmentOne, menuFragment);
ft.replace(R.id.fragmentTwo, baseFragment);
}
ft.commit();
}
This works, at least from time to time. However, many times I get java.lang.IllegalStateException: Fragment already added: MenuFragment (....)
Can anyone please give me some pointers as to how to better implement this? My current code is not pretty at all, and I'm sure many developers out there want to achieve exactly this.
Thanks in advance!
A common way to implement this scenario is to only use the fragment stack when in a mode that shows multiple fragments. When you're in the single fragment mode, you start a new activity that's sole job is to display the single fragment and take advantage of the activity back stack.
In your case you'll just need to remember the currently selected spot on rotate to set it as an argument when starting the new activity.
It's explained much better here:-
http://developer.android.com/guide/components/fragments.html
Hope that helps.

Categories

Resources