Switch between tab navigation and list navigation - android

I am using ActionBarSherlock and applying this pattern for the tab navigation that I found on android developer site. It's working pretty good but I also want to able to switch between NAVIGATION_MODE_TABS and NAVIGATION_MODE_LIST preserving the association between tabs and the fragments.
The pattern I mentioned above is pretty good for preserving a generic code. So I add listeners to my tabs and associate them with specific fragments like this:
bar.addTab(bar.newTab()
.setText("MyFragment")
.setTabListener(new TabListener<SomeFragment>(this, "myfargment", SomeFragment.class)));
and instantiate the fragment when the associated tab is clicked with the help of generics:
public void onTabSelected(Tab tab, FragmentTransaction ft) {
if (mFragment == null) {
mFragment = Fragment.instantiate(mActivity, mClass.getName(), mArgs);
ft.add(android.R.id.content, mFragment, mTag);
} else {
ft.attach(mFragment);
}
}
My question is how can I achieve a similar way while navigating between my fragments with the list navigation mode. I couldn't find a similar way since the OnNavigationListener for the list on the ActionBar works for the whole list instead of per item basis like the tablistener.
or do I have to do something like this:
#Override
public boolean onNavigationItemSelected(int itemPosition, long itemId) {
switch (itemPosition) {
case 0:
//Replace the current fragment with FragmentA
break;
case 1:
//Replace the current fragment with FragmentB
break;
case 2:
//Replace the current fragment with FragmentC
break;
default:
break;
}
return true;
}
EDIT:
I have noticed an interesting behaviour:
While the navigation mode is set to NAVIGATION_MODE_TABS if I put my phone in landscape mode it converts the tabs to a list and preservers the association between the fragments and the list items(which were tab items before) how can I achieve this result on demand rather than on orientation change?

I dont think its possible if you are in tab mode to manually set to a list navigation. I have the list navigation set for one of my applications when it falls below the "Large" bucket. This is how I am using the navigation listener:
OnNavigationListener mNavigationListener = new OnNavigationListener()
{
int mLastPosition = -1;
#Override
public boolean onNavigationItemSelected(int itemPosition, long itemId)
{
String selectedTag = mFragmentNames.get(itemPosition);
FragmentManager fm = getSupportFragmentManager();
Fragment fragment = fm.findFragmentByTag(selectedTag);
FragmentTransaction ft = fm.beginTransaction();
/** Used to avoid the same fragment being reattached. */
if(mLastPosition != itemPosition)
{
/** Means there was a previous fragment attached. */
if(mLastPosition != -1)
{
Fragment lastFragment = fm.findFragmentByTag(mFragmentNames.get(mLastPosition));
if(lastFragment != null)
ft.remove(lastFragment);
}
if(fragment == null)
{
/** The fragment is being added for the first time. */
fragment = Fragment.instantiate(HomeActivity.this, selectedTag);
ft.add(R.id.rootLayout, fragment, selectedTag);
ft.commit();
} else
{
ft.attach(fragment);
ft.commit();
}
}
/**
* The newly attached fragment will be the last position if changed.
*/
mLastPosition = itemPosition;
return true;
}
};
The variable mFragmentNames is a HashMap that maps a integer position to a fragment name ex. "com.android.myproject.MyFragment"
I hope this helps.

Related

Separate Back Stack for each tab in BottomNavigationView Android using Fragments

I'm implementing BottomNavigationView for navigation in an Android app. I am using Fragments to set the content for each tab.
I know how to set up one fragment for each tab and then switch fragments when a tab is clicked. But how can I have a separate back stack for each tab?
Here is the code to set up one fragment:
Fragment selectedFragment = ItemsFragment.newInstance();
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
transaction.replace(R.id.content, selectedFragment);
transaction.commit();
For an example, Fragment A and B would be under Tab 1 and Fragment C and D under Tab 2. When the app is started Fragment A is shown and Tab 1 is selected. Then Fragment A might be replaced with Fragment B. When Tab 2 is selected Fragment C should be displayed. If Tab 1 is then selected Fragment B should once again be displayed. At this point, it should be possible to use the back button to show Fragment A.
And Here is the code to set up next fragment in the same tab:
Fragment selectedFragment = ItemsFragment.newInstance();
FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.replace(R.id.content, selectedFragment);
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
ft.addToBackStack(null);
ft.commit();
Finally, I found the solution, it was inspired by a previous answer on StackOverflow: Separate Back Stack for each tab in Android using Fragments
I only have replaced TabHost with BottomNavigationView and here is the code:
Main Activity
public class MainActivity extends AppCompatActivity {
private HashMap<String, Stack<Fragment>> mStacks;
public static final String TAB_HOME = "tab_home";
public static final String TAB_DASHBOARD = "tab_dashboard";
public static final String TAB_NOTIFICATIONS = "tab_notifications";
private String mCurrentTab;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
BottomNavigationView navigation = (BottomNavigationView) findViewById(R.id.navigation);
navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener);
mStacks = new HashMap<String, Stack<Fragment>>();
mStacks.put(TAB_HOME, new Stack<Fragment>());
mStacks.put(TAB_DASHBOARD, new Stack<Fragment>());
mStacks.put(TAB_NOTIFICATIONS, new Stack<Fragment>());
navigation.setSelectedItemId(R.id.navigation_home);
}
private BottomNavigationView.OnNavigationItemSelectedListener mOnNavigationItemSelectedListener
= new BottomNavigationView.OnNavigationItemSelectedListener() {
#Override
public boolean onNavigationItemSelected(#NonNull MenuItem item) {
switch (item.getItemId()) {
case R.id.navigation_home:
selectedTab(TAB_HOME);
return true;
case R.id.navigation_dashboard:
selectedTab(TAB_DASHBOARD);
return true;
case R.id.navigation_notifications:
selectedTab(TAB_NOTIFICATIONS);
return true;
}
return false;
}
};
private void gotoFragment(Fragment selectedFragment)
{
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.content, selectedFragment);
fragmentTransaction.commit();
}
private void selectedTab(String tabId)
{
mCurrentTab = tabId;
if(mStacks.get(tabId).size() == 0){
/*
* First time this tab is selected. So add first fragment of that tab.
* Dont need animation, so that argument is false.
* We are adding a new fragment which is not present in stack. So add to stack is true.
*/
if(tabId.equals(TAB_HOME)){
pushFragments(tabId, new HomeFragment(),true);
}else if(tabId.equals(TAB_DASHBOARD)){
pushFragments(tabId, new DashboardFragment(),true);
}else if(tabId.equals(TAB_NOTIFICATIONS)){
pushFragments(tabId, new NotificationsFragment(),true);
}
}else {
/*
* We are switching tabs, and target tab is already has atleast one fragment.
* No need of animation, no need of stack pushing. Just show the target fragment
*/
pushFragments(tabId, mStacks.get(tabId).lastElement(),false);
}
}
public void pushFragments(String tag, Fragment fragment, boolean shouldAdd){
if(shouldAdd)
mStacks.get(tag).push(fragment);
FragmentManager manager = getSupportFragmentManager();
FragmentTransaction ft = manager.beginTransaction();
ft.replace(R.id.content, fragment);
ft.commit();
}
public void popFragments(){
/*
* Select the second last fragment in current tab's stack..
* which will be shown after the fragment transaction given below
*/
Fragment fragment = mStacks.get(mCurrentTab).elementAt(mStacks.get(mCurrentTab).size() - 2);
/*pop current fragment from stack.. */
mStacks.get(mCurrentTab).pop();
/* We have the target fragment in hand.. Just show it.. Show a standard navigation animation*/
FragmentManager manager = getSupportFragmentManager();
FragmentTransaction ft = manager.beginTransaction();
ft.replace(R.id.content, fragment);
ft.commit();
}
#Override
public void onBackPressed() {
if(mStacks.get(mCurrentTab).size() == 1){
// We are already showing first fragment of current tab, so when back pressed, we will finish this activity..
finish();
return;
}
/* Goto previous fragment in navigation stack of this tab */
popFragments();
}
}
Home fragment example
public class HomeFragment extends Fragment {
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_home, container, false);
Button gotoNextFragment = (Button) view.findViewById(R.id.gotoHome2);
gotoNextFragment.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
((MainActivity)getActivity()).pushFragments(MainActivity.TAB_HOME, new Home2Fragment(),true);
}
});
return view;
}
}
This behavior is supported by the new Navigation Architecture Component (https://developer.android.com/topic/libraries/architecture/navigation/).
Essentially, one can use NavHostFragment, which is a fragment that controls its own back stack:
Each NavHostFragment has a NavController that defines valid navigation within the navigation host. This includes the navigation graph as well as navigation state such as current location and back stack that will be saved and restored along with the NavHostFragment itself.
https://developer.android.com/reference/androidx/navigation/fragment/NavHostFragment
Here is an example: https://github.com/deisold/navigation
Edit: Turns out Navigation Architecture Component doesn't support seperate back stacks anyway, as pointed out by the commenters. But as #r4jiv007 mentioned, they are working on it and has offered an "official hack" in the meantime: https://github.com/googlesamples/android-architecture-components/tree/master/NavigationAdvancedSample
It is worth noting that the behavior you describe goes against the Google guidelines. https://material.io/guidelines/components/bottom-navigation.html#bottom-navigation-behavior
Navigation through the bottom navigation bar should reset the task state.
In other words, having Fragment A and Fragment B "inside" Tab 1 is fine, but if the user opens Fragment B, clicks Tab 2, and then clicks Tab 1 again, they should see Fragment A.
Suppose you have 5(A, B, C, D, E) BottomNavigationView menu item, then in Activity create 5 FrameLayout(frmlytA, frmlytB, frmlytC, frmlytD, frmlytE) in parallel overlapping manner as the container for each of these menu items. When BottomNavigation Menu item A is pressed then hide all the other FrameLayouts(Visibility = GONE) and just show(Visibility = VISIBLE) the FrameLayout 'frmlytA' which will host the FragmentA and over this container do the further transactions like (FragmentA -> FragmentX -> FragmentY). And then If user clicks BottomNavigation Menu item B then just hide this(frmlytA) container and show 'frmlytB'. Then if user again presses the menu item A then show 'frmlytA' it should retain the earlier state. So, like this you can switch between the container FrameLayouts and can maintain the back stack of each container.
Instead of using replace method use add fragment,
Instead of this method
ft.replace(R.id.content, selectedFragment);
Use this
ft.add(R.id.content, selectedFragment);
Fragment selectedFragment = ItemsFragment.newInstance();
FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.(R.id.content, selectedFragment);
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
ft.addToBackStack(null);
ft.commit();

Fragment Transaction load empty view but fragment is shown after rotating device

I am building a navigation drawer as designed by the google documentation however I have an issue where the fragment is not being replaced. http://developer.android.com/training/implementing-navigation/nav-drawer.html
When the app first loads, the default fragment is loaded.
Clicking on another item on the drawer list leaves an empty view
However on rotating the device, loads the fragment chosen.
public void selectNavActivty(int position){
// TODO Changing between the different screens selection
fragment = null;
switch (position) {
case 0:
fragment = OverLay.newInstance();
break;
case 1:
fragment = Dummy.newInstance();
break;
}
if(fragment != null) {
// attach added to handle viewpager fragments
FragmentTransaction trans = getSupportFragmentManager().beginTransaction();
trans.replace(R.id.content_frame, fragment).attach(fragment)
.addToBackStack(null);
trans.commit();
getFragmentManager().executePendingTransactions();
} else {
Log.d("Drawer Activity","Error in creating Fragment");
}
}
For navigation menu fragment transactions I use the following approach, this way the fragment will be added and placed on top.
String name = "myFragment";
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.content_frame, fragment, name)
.commit();
Look up the attach() function. It follows a different fragment lifecycle.
Also make sure that your layout files framelayout is visible.
Modify your code as below:
if(fragment != null) {
// attach added to handle viewpager fragments
FragmentTransaction trans = getSupportFragmentManager().beginTransaction();
trans.replace(R.id.content_frame, fragment);
trans.addToBackStack(null);
trans.commit();
} else {
Log.d("Drawer Activity","Error in creating Fragment");
}
If the solution doesn't work for you, share the xml code along with your fragment code.
After adding Fragment it will be added to activity state and its view
will be added to defined Container view. But by attaching nothing will
be displayed if fragment was not already added to UI. It just attaches
to fragment manager. However if view was already added to a container
in UI and detached after that, by attaching it will be displayed again
in its container. Finally you can use attach and detach if you want to
destroy fragment View temporarily and want to display and build its
view on future without losing its state inside activity.
https://stackoverflow.com/a/18979024/3329488
My solution is to tag all the fragment with unique tag on fragment replacement. Make sure you also assign a unique tag to the default fragment during it creation. A more efficient way is to identify the fragment before you recreate the same one.
public void selectNavActivty(int position){
// TODO Changing between the different screens selection
FragmentManager fragmentManager = getSupportFragmentManager();
fragment = fragmentManager.findFragmentById(R.id.content_frame);
String fragmentTag = null;
switch (position) {
case 0:
fragmentTag = "case0Tag"; // please change to better tag name
break;
case 1:
fragmentTag = "case1Tag"; // please change to better tag name
break;
default:
Log.d("Drawer Activity","Error in creating Fragment");
return;
}
if (fragmentTag != null && !fragment.getTag().equals(fragmentTag))
fragmentManager.beginTransaction().replace(R.id.content_fragment, fragment, tag).commit();
}
In my case after rotating a device a blank fragment was shown. I understood that in an Activity.onCreate() I always called creating a blank Fragment and after that a needed one. So I changed it's behaviour to this:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState == null) {
openEmptyFragment()
openAnotherFragment()
}
}
I recommend to check savedInstanceState != null before adding new fragments, as written in Why won't Fragment retain state when screen is rotated?.

Android Navigation Drawer Fragment State

I'm currently utilizing the Navigation Drawer for my Android APP. In my first fragment, I've a fragment that loads data using Facebook's Graph API. Thus, when my App is first loaded, it first goes to the first fragment.
Then, I use the Navigation Drawer to click on another Fragment and view it.
And then finally, I reuse the Navigation Drawer to proceed back to the first Fragment and view it.
My issue that I'm facing is, how do I proceed to utilize the Fragment that has been created once instead of re-creating it whenever the Navigation Drawer Item is selected. My code for the switching of the fragments are as shown below.
private void displayView(int position) {
// update the main content by replacing fragments
Fragment fragment = null;
switch (position) {
case 0:
fragment = new SelectionFragment();
break;
case 1:
fragment = new HomeFragment();
break;
case 2:
fragment = new PhotosFragment();
break;
case 3:
fragment = new CommunityFragment();
break;
case 4:
fragment = new PagesFragment();
break;
case 5:
fragment = new SplashFragment();
break;
default:
break;
}
if (fragment != null) {
FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.beginTransaction()
.replace(R.id.frame_container, fragment).commit();
// update selected item and title, then close the drawer
mDrawerList.setItemChecked(position, true);
mDrawerList.setSelection(position);
setTitle(navMenuTitles[position]);
mDrawerLayout.closeDrawer(mDrawerList);
} else {
// error in creating fragment
Log.e("MainActivity", "Error in creating fragment");
}
}
I noticed that there is indeed a "new" instance of the Fragment every time whenever the option is selected. How do I go about implementing the logic of creating the Fragment instance ONCE and reusing it, so that I do not have to continuously load the Fragment over and over again.
To anyone who encounters the same issue with me,I've managed to find a solution.
In the container frame,I've to define specific fragment views that I'll be utilizing as shown below.
In each Fragment view,I've to "link" it with the actual Fragment itself as shown below via the "android:name" attribute.Do take note of the the path to the Fragment,example for in my case it's com.example.confesssionsrp.SelectionFragment.
<fragment
android:id="#+id/selectionFragment"
android:name="com.example.confessionsrp.SelectionFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
In the the MainActivity(or the Activity where we're viewing the fragments),create variables for each of the Fragments as shown below.
Following that,in the Oncreate of the MainActivity(or your specific Activity),initialize the various fragments as shown below.
Proceed onto creating a new Method called "ShowFragment" as shown below.
private void showFragment(int fragmentIndex, boolean addToBackStack) {
FragmentManager fm = getSupportFragmentManager();
FragmentTransaction transaction = fm.beginTransaction();
for (int i = 0; i < fragments.length; i++) {
if (i == fragmentIndex) {
transaction.show(fragments[i]);
if (Session.getActiveSession().isClosed()) {
mDrawerLayout
.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
}
} else {
transaction.hide(fragments[i]);
}
}
if (addToBackStack) {
transaction.addToBackStack(null);
}
transaction.commit();
}
From then on in the switching of the fragments,manually call upon the "ShowFragment" method with the selected Fragment as shown below.
Doing all of this overall,will not reset the Fragment each time we view it,and therefore is the solution to the answer.Thank you for anyone who has helped me so far :)!
I am using the following code:
#Override
public void onNavigationDrawerItemSelected(int position) {
// update the main content by replacing fragments
FragmentManager fragmentManager = getFragmentManager();
if(position==0){// selection of tabs content
fragmentManager
.beginTransaction()
.replace(R.id.container,
SimulatorFragment.newInstance(position + 1)).commit();
}else if(position==1){
fragmentManager
.beginTransaction()
.replace(R.id.container,
HudFragment.newInstance(position + 1)).commit();
}else if(position==2){
// Display the fragment as the main content.
fragmentManager
.beginTransaction()
.replace(R.id.container,
SettingsBasicFragment.newInstance(position +1)).commit();
}else{
}
}
You can replace by a new instance the first time and store the fragment, if it is not null, then replace by the stored fragment.
The activity must implement NavigationDrawerFragment.NavigationDrawerCallbacks
The fragment constructor and newInstance methods look like this:
public final class HudFragment extends Fragment {
/**
* The fragment argument representing the section number for this
* fragment.
*/
private static final String ARG_SECTION_NUMBER = "section_number";
/**
* Returns a new instance of this fragment for the given section number.
* #param simulation
*/
public static HudFragment newInstance(int sectionNumber) {
HudFragment fragment = new HudFragment();
Bundle args = new Bundle();
args.putInt(ARG_SECTION_NUMBER, sectionNumber);
fragment.setArguments(args);
return fragment;
}
public HudFragment() {
}
To switch fragments by code I use this method inside the NavigationDrawerFragment:
/**
* Select a different section
* #param position
*/
public void select(int position){
selectItem(position);
}
private void selectItem(int position) {
mCurrentSelectedPosition = position;
if (mDrawerListView != null) {
mDrawerListView.setItemChecked(position, true);
}
if (mDrawerLayout != null) {
mDrawerLayout.closeDrawer(mFragmentContainerView);
}
if (mCallbacks != null) {
mCallbacks.onNavigationDrawerItemSelected(position);
}
}
The second option is to start with the example of navigationDrawer that Android SDK offers. I selected that template of activity when creating the project and almost all the code of my previous answer is produced automatically.
If you want to keep the fragments after device rotation or similars it is a different thing, you need then to retain the fragments. If not, you just need to save the new instance of the fragment in a variable and check if it is null to create a new one or use the old one.
In case someone want's a different approach to this: you could find the fragment on the stack:
// check if this fragment is on the backstack to avoid creating a new one
Fragment foundFragment = fragmentManager.findFragmentByTag("unique_fragment_tag");
if (foundFragment != null) {
fragmentManager.popBackStackImmediate("unique_fragment_tag", 0);
return;
}

How can I switch between two fragments, without recreating the fragments each time?

I'm working on an android application, that uses a navigation drawer to switch between two fragments. However, each time I switch, the fragment is completely recreated.
Here is the code from my main activity.
/* The click listener for ListView in the navigation drawer */
private class DrawerItemClickListener implements ListView.OnItemClickListener {
#Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
selectItem(position);
}
}
private void selectItem(int position) {
android.support.v4.app.Fragment fragment;
String tag;
android.support.v4.app.FragmentManager; fragmentManager = getSupportFragmentManager();
switch(position) {
case 0:
if(fragmentManager.findFragmentByTag("one") != null) {
fragment = fragmentManager.findFragmentByTag("one");
} else {
fragment = new OneFragment();
}
tag = "one";
break;
case 1:
if(fragmentManager.findFragmentByTag("two") != null) {
fragment = fragmentManager.findFragmentByTag("two");
} else {
fragment = new TwoFragment();
}
tag = "two";
break;
}
fragment.setRetainInstance(true);
fragmentManager.beginTransaction().replace(R.id.container, fragment, tag).commit();
// update selected item and title, then close the drawer
mDrawerList.setItemChecked(position, true);
setTitle(mNavTitles[position]);
mDrawerLayout.closeDrawer(mDrawerList);
}
I've set up some debug logging, and every time selectItem is called, one fragment is destroyed, while the other is created.
Is there any way to prevent the fragments from being recreated, and just reuse them instead?
After #meredrica pointed out that replace() destroys the fragments, I went back through the FragmentManager documentation. This is the solution I've come up with, that seems to be working.
/* The click listener for ListView in the navigation drawer */
private class DrawerItemClickListener implements ListView.OnItemClickListener {
#Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
selectItem(position);
}
}
private void selectItem(int position) {
android.support.v4.app.FragmentManager; fragmentManager = getSupportFragmentManager();
switch(position) {
case 0:
if(fragmentManager.findFragmentByTag("one") != null) {
//if the fragment exists, show it.
fragmentManager.beginTransaction().show(fragmentManager.findFragmentByTag("one")).commit();
} else {
//if the fragment does not exist, add it to fragment manager.
fragmentManager.beginTransaction().add(R.id.container, new OneFragment(), "one").commit();
}
if(fragmentManager.findFragmentByTag("two") != null){
//if the other fragment is visible, hide it.
fragmentManager.beginTransaction().hide(fragmentManager.findFragmentByTag("two")).commit();
}
break;
case 1:
if(fragmentManager.findFragmentByTag("two") != null) {
//if the fragment exists, show it.
fragmentManager.beginTransaction().show(fragmentManager.findFragmentByTag("two")).commit();
} else {
//if the fragment does not exist, add it to fragment manager.
fragmentManager.beginTransaction().add(R.id.container, new TwoFragment(), "two").commit();
}
if(fragmentManager.findFragmentByTag("one") != null){
//if the other fragment is visible, hide it.
fragmentManager.beginTransaction().hide(fragmentManager.findFragmentByTag("one")).commit();
}
break;
}
// update selected item and title, then close the drawer
mDrawerList.setItemChecked(position, true);
setTitle(mNavTitles[position]);
mDrawerLayout.closeDrawer(mDrawerList);
}
I also added this bit, but I'm not sure if it's necessary or not.
#Override
public void onDestroy() {
super.onDestroy();
FragmentManager fragmentManager = getSupportFragmentManager();
if(fragmentManager.findFragmentByTag("one") != null){
fragmentManager.beginTransaction().remove(fragmentManager.findFragmentByTag("one")).commit();
}
if(fragmentManager.findFragmentByTag("two") != null){
fragmentManager.beginTransaction().remove(fragmentManager.findFragmentByTag("two")).commit();
}
}
Use the attach/detach method with tags:
Detach will destroy the view hirachy but keeps the state, like if on the backstack; this will let the "not-visible" fragment have a smaller memory footprint. But mind you that you need to correctly implement the fragment lifecycle (which you should do in the first place)
Detach the given fragment from the UI. This is the same state as when it is put on the back stack: the fragment is removed from the UI, however its state is still being actively managed by the fragment manager. When going into this state its view hierarchy is destroyed.
The first time you add the fragment
FragmentTransaction t = getSupportFragmentManager().beginTransaction();
t.add(android.R.id.content, new MyFragment(),MyFragment.class.getSimpleName());
t.commit();
then you detach it
FragmentTransaction t = getSupportFragmentManager().beginTransaction();
t.detach(MyFragment.class.getSimpleName());
t.commit();
and attach it again if switched back, state will be kept
FragmentTransaction t = getSupportFragmentManager().beginTransaction();
t.attach(getSupportFragmentManager().findFragmentByTag(MyFragment.class.getSimpleName()));
t.commit();
But you always have to check if the fragment was added yet, if not then add it, else just attach it:
if (getSupportFragmentManager().findFragmentByTag(MyFragment.class.getSimpleName()) == null) {
FragmentTransaction t = getSupportFragmentManager().beginTransaction();
t.add(android.R.id.content, new MyFragment(), MyFragment.class.getSimpleName());
t.commit();
} else {
FragmentTransaction t = getSupportFragmentManager().beginTransaction();
t.attach(getSupportFragmentManager().findFragmentByTag(MyFragment.class.getSimpleName()));
t.commit();
}
The replace method destroys your fragments. One workaround is to set them to Visibility.GONE, another (less easy) method is to hold them in a variable. If you do that, make sure you don't leak memory left and right.
I did this before like this:
if (mPrevFrag != fragment) {
// Change
FragmentTransaction ft = fragmentManager.beginTransaction();
if (mPrevFrag != null){
ft.hide(mPrevFrag);
}
ft.show(fragment);
ft.commit();
mPrevFrag = fragment;
}
(you will need to track your pervious fragment in this solution)
I guess you can not directly manipulate the lifecycle mechanisms of your Fragments. The very fact that you can findFragmentByTag is not very bad. It means that the Fragment object is not recreated fully, if it is already commited. The existing Fragment just passes all the lifecycle steps each Fragment has - that means that only UI is "recreated".
It is a very convenient and useful memory management strategy - and appropriate, in most cases. Fragment which is gone, has the resources which have to be utilized in order to de-allocate memory.
If you just cease using this strategy, the memory usage of your application could increase badly.
Nonetheless, there are retained fragments, which lifecycle is a bit different and do not correspond to the Activity they are attached to. Typically, they are used to retain some things you want to save, for example, to manage configuration changes
However, the fragment [re]creation strategy depends on the context - that is, what you would like to solve, and what are the trade-offs that you are willing to accept.
Just find the current fragment calling getFragmentById("id of your container") and then hide it and show needed fragment.
private void openFragment(Fragment fragment, String tag) {
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
Fragment existingFragment = fragmentManager.findFragmentByTag(tag);
if (existingFragment != null) {
Fragment currentFragment = fragmentManager.findFragmentById(R.id.container);
fragmentTransaction.hide(currentFragment);
fragmentTransaction.show(existingFragment);
}
else {
fragmentTransaction.add(R.id.container, fragment, tag);
}
fragmentTransaction.commit();
}
Same idea as Tester101 but this is what I ended up using.
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
Fragment oldFragment = fragmentManager.findFragmentByTag( "" + m_lastDrawerSelectPosition );
if ( oldFragment != null )
fragmentTransaction.hide( oldFragment );
Fragment newFragment = fragmentManager.findFragmentByTag( "" + position );
if ( newFragment == null )
{
newFragment = getFragment( position );
fragmentTransaction.add( R.id.home_content_frame, newFragment, "" + position );
}
fragmentTransaction.show( newFragment );
fragmentTransaction.commit();
Hide easily in kotlin using extensions:
fun FragmentManager.present(newFragment: Fragment, lastFragment: Fragment? = null, containerId: Int) {
if (lastFragment == newFragment) return
val transaction = beginTransaction()
if (lastFragment != null && findFragmentByTag(lastFragment.getTagg()) != null) {
transaction.hide(lastFragment)
}
val existingFragment = findFragmentByTag(newFragment.getTagg())
if (existingFragment != null) {
transaction.show(existingFragment).commit()
} else {
transaction.add(containerId, newFragment, newFragment.getTagg()).commit()
}
}
fun Fragment.getTagg(): String = this::class.java.simpleName
Usage
supportFragmentManager.present(fragment, lastFragment, R.id.fragmentPlaceHolder)
lastFragment = fragment
Here's what I'm using for a simple 2 fragment case in Kotlin:
private val advancedHome = HomeAdvancedFragment()
private val basicHome = HomeBasicFragment()
override fun onCreate(savedInstanceState: Bundle?) {
...
// Attach both fragments and hide one so we can swap out easily later
supportFragmentManager.commit {
setReorderingAllowed(true)
add(R.id.fragment_container_view, basicHome)
add(R.id.fragment_container_view, advancedHome)
hide(basicHome)
}
binding.displayModeToggle.onStateChanged {
when (it) {
0 -> swapFragments(advancedHome, basicHome)
1 -> swapFragments(basicHome, advancedHome)
}
}
...
}
With this FragmentActivity extension:
fun FragmentActivity.swapFragments(show: Fragment, hide: Fragment) {
supportFragmentManager.commit {
show(show)
hide(hide)
}
}
How about playing with the Visible attribute?
this is a little late response.
if you're using view pager for fragments, set the off screen page limit of the fragment to the number of fragments created.
mViewPager.setOffscreenPageLimit(3); // number of fragments here is 3

Using Fragments and TabListener the right way

My problem is about fragments with tabs. I have got one activity which basically has an empty layout and I add fragments to it dynamically. As soon as I start the activity I add a fragment with a listview to it and when I click on an item of the list I remove the fragment with the listview and add another fragment with details about it. Also some tabs are visible now. Clicking on one of these tabs shows other details about the item from the listview.
I do have working code but it seems that I'm hacking it altogether.
Here is some bits of it:
Activity:
// In onCreate:
#Override
protected void onCreate(Bundle savedInstanceState) {
// Check that the activity is using the layout version with
// the fragment_container FrameLayout
if (findViewById(R.id.fragment_container) != null) {
// However, if we're being restored from a previous state,
// then we don't need to do anything and should return or else
// we could end up with overlapping fragments.
if (savedInstanceState != null) {
return;
}
// Create an instance of FList
fList = new FList();
// Add the fragment to the 'fragment_container' FrameLayout
getFragmentManager().beginTransaction().add(R.id.fragment_container,fList).commit();
}
}
// After clicking on an item of the listview changeFragment() is called
public void changeFragment() {
FragmentTransaction ft = getFragmentManager().beginTransaction();
fSpeed = new FSpeed();
ft.add(R.id.fragment_container, fSpeed);
// this might be necessary
if (actionBar != null)
actionBar.selectTab(tabSpeed);
ft.commit();
}
// In the activity I also setup the ActionBar with tabs
private void setupActionBar() {
tabSpeed = actionBar.newTab();
// and a few more tabs...
tabSpeed.setTabListener(this);
actionBar.addTab(tabSpeed);
// But the tabs are not visible because of the navigation mode
actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
}
// Here is the tabListener - very hacky!
#Override
public void onTabSelected(Tab tab, FragmentTransaction ft)
if (fm == null)
fm = getFragmentManager();
Fragment f = null;
switch (tab.getPosition()) {
case 0:
if (fSpeed == null)
fSpeed = new FSpeed();
f = findFragment(fSpeed);
if (f != null) {
ft.setCustomAnimations(R.anim.fragment_alpha_0_1, R.anim.fragment_alpha_1_0);
ft.remove(f);
ft.add(R.id.fragment_container, fSpeed);
} else {
Log.d(TAG, "fSpeed is " + f);
return;
}
break;
case 1:
// more tabs - similar code
}
// the findFragment() method
private Fragment findFragment(Fragment selectedFragment) {
if (fm.findFragmentById(R.id.fragment_container) == fSpeed && selectedFragment != fSpeed)
return fSpeed;
// more ifs for other fragments
else
return null;
}
This all worked fine for the minute but after switching x times between the tabs things started looking funny, e.g. fList was visible, tab icons disappeared, ... I could track the bugs down and add some null checks but I think I went the wrong way. This is why I changed the TabListener with the following one:
// Still in activity
public static class MyTabListener<T extends Fragment> implements TabListener {
private Fragment mFragment;
private final Activity mActivity;
private final String mTag;
private final Class<T> mClass;
/**
* * Constructor used each time a new tab is created.
* * * #param
* activity * The host Activity, used to instantiate the fragment
* * #param
* tag * The identifier tag for the fragment
* * #param clz * The
* fragment's Class, used to instantiate the fragment
*/
public MyTabListener(Activity activity, String tag, Class<T> clz) {
mActivity = activity;
mTag = tag;
mClass = clz;
}
/* The following are each of the ActionBar.TabListener callbacks */
public void onTabSelected(Tab tab, FragmentTransaction ft) {
// Check if the fragment is already initialized
if (mFragment == null) {
// If not, instantiate and add it to the activity
mFragment = Fragment.instantiate(mActivity, mClass.getName());
ft.add(R.id.fragment_container, mFragment, mTag);
} else {
// If it exists, simply attach it in order to show it
ft.attach(mFragment);
}
}
public void onTabUnselected(Tab tab, FragmentTransaction ft) {
if (mFragment != null) {
ft.detach(mFragment);
}
}
// The tabs are now assigned to MyTabListener
tabSpeed.setTabListener(new MyTabListener<FSpeed>(this, "speed", FSpeed.class));
Here is where the problem started. After clicking on an item from the listview with the new TabListener onTabSelected and onTabUnSelected are called in turns endlessly.
Question:
My question is how do you go from a fragment with a listview to another fragment after clicking on an item of the listview (and still use the same activity). Is my own written method changeFragment() the wrong approach? Still I would like to use the MyTabListener.
Note 1: I bought Commonsware's book to learn more about fragments but I couldn't find a more complex example with different fragments working together and also couldn't find how the back button is handled, or overridden. For example after clicking on the back button if fragment1, 2 or 3 are visible always show fragment4. If anybody found one in the book could you please tell me the chapter (name)/ page? If there isn't it would be very kind of the Common guys to provide one in the next update or so.
Note 2: Global variables were used in the project like fSpeed, tabSpeed, ...
Note 3: If you need more code or explanation please let me know in the comments. Thanks for helping!
Is my own written method changeFragment() the wrong approach?
You are adding a fragment, not replacing one. To replace a fragment, use replace(), not add().

Categories

Resources