Modifying Sherlock Master Detail Flow - android

if you've used the sherlock master detail flow, please help me.
I have added tabs and have removed the data inside the detail fragment/activity but when I try to inflate a button inside the tabs, it doesn't work.
Can you help me?
Here's the list activity that I've modified to display tabs when in the two-pane mode.
package com.example.sample;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction;
import com.actionbarsherlock.app.ActionBar;
import com.actionbarsherlock.app.ActionBar.Tab;
import com.actionbarsherlock.app.SherlockFragmentActivity;
/**
* An activity representing a list of Courses. This activity has different
* presentations for handset and tablet-size devices. On handsets, the activity
* presents a list of items, which when touched, lead to a
* {#link CourseDetailActivity} representing item details. On tablets, the
* activity presents the list of items and item details side-by-side using two
* vertical panes.
* <p>
* The activity makes heavy use of fragments. The list of items is a
* {#link CourseListFragment} and the item details (if present) is a
* {#link CourseDetailFragment}.
* <p>
* This activity also implements the required
* {#link CourseListFragment.Callbacks} interface to listen for item selections.
*/
public class CourseListActivity extends SherlockFragmentActivity implements
CourseListFragment.Callbacks {
/**
* Whether or not the activity is in two-pane mode, i.e. running on a tablet
* device.
*/
private boolean mTwoPane;
private boolean once = true;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_course_list);
if (findViewById(R.id.course_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.
((CourseListFragment) getSupportFragmentManager().findFragmentById(
R.id.course_list)).setActivateOnItemClick(true);
}
// TODO: If exposing deep links into your app, handle intents here.
}
/**
* Callback method from {#link CourseListFragment.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.
CourseDetailFragment fragment = new CourseDetailFragment();
getSupportFragmentManager().beginTransaction()
.replace(R.id.course_detail_container, fragment).commit();
if (once) {
ActionBar actionBar = getSupportActionBar();
actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
// initiating both tabs and set text to it.
ActionBar.Tab assignTab = actionBar.newTab().setText("Assignments");
ActionBar.Tab schedTab = actionBar.newTab().setText("Schedule");
ActionBar.Tab contactTab = actionBar.newTab().setText("Contact");
// Create three fragments to display content
Fragment assignFragment = new Assignments();
Fragment schedFragment = new Schedule();
Fragment contactFragment = new Contact();
assignTab.setTabListener(new MyTabsListener(assignFragment));
schedTab.setTabListener(new MyTabsListener(schedFragment));
contactTab.setTabListener(new MyTabsListener(contactFragment));
actionBar.addTab(assignTab);
actionBar.addTab(schedTab);
actionBar.addTab(contactTab);
once = false;
}
} else {
// In single-pane mode, simply start the detail activity
// for the selected item ID.
Intent detailIntent = new Intent(this, CourseDetailActivity.class);
startActivity(detailIntent);
}
}
class MyTabsListener implements ActionBar.TabListener {
public Fragment fragment;
public MyTabsListener(Fragment fragment) {
this.fragment = fragment;
}
#Override
public void onTabReselected(Tab tab, FragmentTransaction ft) {
}
#Override
public void onTabSelected(Tab tab, FragmentTransaction ft) {
ft.replace(R.id.twopanecontainer, fragment);
}
#Override
public void onTabUnselected(Tab tab, FragmentTransaction ft) {
ft.remove(fragment);
}
}
}
Here's the fragment course detail layout that holds a textview by default.
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/twopanecontainer"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<TextView
android:id="#+id/course_detail"
style="?android:attr/textAppearanceLarge"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
tools:context=".CourseDetailFragment" />
</LinearLayout>
What should I modify so I can inflate a different view for each tab?
Thanks

Have a ViewPager in yout activity layout, then implement FragmentPagerAdapter

Related

ActionBar compact with 3 tabs: issue with fragments

I'm currently working in a new android project .
I'm using ActionBar Compact with Tab navigation mode .
I added 3 tabs in my Activity.
In the first Fragment , there is a listView and TextView (witch contains the text of slected row of list)
Now , when i select the second tab (fragment (empty) ), and go back to my first fragment , the TextView contains the selected value
BUT when I select the third tab (fragment 3 empty) , and go back to my first fragent , the TextView was initilised .
I think that my problem is in the TabListener
can anyone help me please !
this is the code (simplified)
MainActivity:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
/** Getting a reference to action bar of this activity */
mActionbar = getSupportActionBar();
/** Set tab navigation mode */
mActionbar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
/** set HomeButton to true */
mActionbar.setHomeButtonEnabled(true);
/** Getting a reference to ViewPager from the layout */
mPager = (ViewPager) findViewById(R.id.pager);
/** Getting a reference to FragmentManager */
FragmentManager fm = getSupportFragmentManager();
/** Defining a listener for pageChange */
ViewPager.SimpleOnPageChangeListener pageChangeListener = new ViewPager.SimpleOnPageChangeListener(){
#Override
public void onPageSelected(int position) {
super.onPageSelected(position);
mActionbar.setSelectedNavigationItem(position);
}
};
/** Setting the pageChange listener to the viewPager */
mPager.setOnPageChangeListener(pageChangeListener);
/** Creating an instance of FragmentPagerAdapter */
MyFragmentPagerAdapter fragmentPagerAdapter = new MyFragmentPagerAdapter(fm);
/** Setting the FragmentPagerAdapter object to the viewPager object */
mPager.setAdapter(fragmentPagerAdapter);
mActionbar.setDisplayShowTitleEnabled(true);
/** Defining tab listener */
ActionBar.TabListener tabListener = new ActionBar.TabListener() {
#Override
public void onTabUnselected(Tab tab, FragmentTransaction ft) {
}
#Override
public void onTabSelected(Tab tab, FragmentTransaction ft) {
mPager.setCurrentItem(tab.getPosition());
}
#Override
public void onTabReselected(Tab tab, FragmentTransaction ft) {
}
};
/** Creating fragment1 Tab */
Tab tab = mActionbar.newTab()
.setText("Categories")
.setTabListener(tabListener);
mActionbar.addTab(tab, 0, false);
/** Creating fragment2 Tab */
tab = mActionbar.newTab()
.setText("Acceuil")
.setTabListener(tabListener);
mActionbar.addTab(tab, 1, true);
/** Creating fragment3 Tab */
tab = mActionbar.newTab()
.setText("Services")
.setTabListener(tabListener);
mActionbar.addTab(tab, 2, false);
}
Fragment 2 ::
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
categories = inflater.inflate(R.layout.fragment_categories, container, false);
list= (ListView)categories.findViewById(R.id.listCategories);
// Defined Array values to show in ListView
String[] values = new String[] { "Android List View",
"Adapter implementation",
"Simple List View In Android",
"Create List View Android",
"Android Example",
"List View Source Code",
"List View Array Adapter",
"Android Example List View"
};
ArrayAdapter<String> adapter = new ArrayAdapter<String>(getActivity().getApplicationContext(),android.R.layout.simple_list_item_1, android.R.id.text1, values);
// Assign adapter to ListView
list.setAdapter(adapter);
t =(TextView)categories.findViewById(R.id.textView1);
list.setOnItemClickListener(new OnItemClickListener()
{
public void onItemClick(AdapterView<?> parent,View v, int position, long id)
{
Toast.makeText(getActivity().getBaseContext(),"category " + (position + 1) +" selected",Toast.LENGTH_SHORT).show();
t.setText("category " + (position + 1) +" selected");
}
});
return categories;
}
ScreenShots
This when I select row from listView (fragment 1) ==> fragment 2 ==> go back fragment 1
This when I select row from listView (fragment 1) ==> fragment 3 ==> go back fragment 1
In onCreate() of your main Activity, after you initialize your ViewPager, add:
mPager.setOffscreenPageLimit(2);
Per the Android docs, setOffscreenPageLimit():
Sets the number of pages that should be retained to either side of the current page in the view hierarchy in an idle state. Pages beyond this limit will be recreated from the adapter when needed.
This is offered as an optimization. If you know in advance the number of pages you will need to support or have lazy-loading mechanisms in place on your pages, tweaking this setting can have benefits in perceived smoothness of paging animations and interaction. If you have a small number of pages (3-4) that you can keep active all at once, less time will be spent in layout for newly created view subtrees as the user pages back and forth.
You should keep this limit low, especially if your pages have complex layouts. This setting defaults to 1.
Since the default setting is 1, when you start on the first tab and navigate to the third tab, the first page (i.e., fragment) is being recreated when you navigate back to the first tab. Setting this to 2 should retain the fragment in the first tab even when you navigate to the third tab.
try using setRetainInstance() inside your fragments
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
This way your fragment instance is not destroyed, only its view hierarchy.

FragmentManager and screen orientation changes in Android

I don't understand the Fragment lifecycle in Android, and what happens during screen orientation changes.
I started with the Master-Detail example in the Android SDK, and I have added the following lines of code:
in MyItemListActivity I modified onCreate()
public class MyItemListActivity extends FragmentActivity implements
MyItemListFragment.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);
FragmentManager fragMgr = getSupportFragmentManager();
MyItemListFragment oldFragment = (MyItemListFragment)fragMgr.findFragmentByTag("booFragment");
if (null == oldFragment) {
FragmentTransaction xact = fragMgr.beginTransaction();
MyItemListFragment newFragment = MyItemListFragment.createInstance("boo");
xact.add(
R.id.myitem_list,
newFragment,
"booFragment");
xact.commit();
}
setContentView(R.layout.activity_myitem_list);
if (findViewById(R.id.myitem_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.
((MyItemListFragment) getSupportFragmentManager().findFragmentById(
R.id.myitem_list)).setActivateOnItemClick(true);
}
// TODO: If exposing deep links into your app, handle intents here.
}
/**
* Callback method from {#link MyItemListFragment.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(MyItemDetailFragment.ARG_ITEM_ID, id);
MyItemDetailFragment fragment = new MyItemDetailFragment();
fragment.setArguments(arguments);
getSupportFragmentManager().beginTransaction()
.replace(R.id.myitem_detail_container, fragment).commit();
} else {
// In single-pane mode, simply start the detail activity
// for the selected item ID.
Intent detailIntent = new Intent(this, MyItemDetailActivity.class);
detailIntent.putExtra(MyItemDetailFragment.ARG_ITEM_ID, id);
startActivity(detailIntent);
}
}
}
in MyItemListFragment I created createInstance()
public class MyItemListFragment 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 MyItemListFragment() {
}
public static MyItemListFragment createInstance(String boo) {
Bundle init = new Bundle();
init.putString(
"booboo",
boo);
MyItemListFragment frag = new MyItemListFragment();
frag.setArguments(init);
return frag;
}
#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;
}
}
The app runs fine when started, but when I rotate the screen the app crashes and the following is in the logs:
12-12 13:41:23.930: E/AndroidRuntime(31051): java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.mymasterdetail/com.example.mymasterdetail.MyItemListActivity}: android.view.InflateException: Binary XML file line #24: Error inflating class fragment
...
12-12 13:41:23.930: E/AndroidRuntime(31051): Caused by: android.view.InflateException: Binary XML file line #24: Error inflating class fragment
12-12 13:41:23.930: E/AndroidRuntime(31051): at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:713)
...
12-12 13:41:23.930: E/AndroidRuntime(31051): at com.android.internal.policy.impl.PhoneWindow.setContentView(PhoneWindow.java:290)
12-12 13:41:23.930: E/AndroidRuntime(31051): at android.app.Activity.setContentView(Activity.java:1928)
12-12 13:41:23.930: E/AndroidRuntime(31051): at com.example.mymasterdetail.MyItemListActivity.onCreate(MyItemListActivity.java:52)
...
(Line 52 is the call to setContentView() in the Activity.)
If I remove xact.add() then the app runs just fine. (But no data is passed to the Fragment.)
I know that the FragmentTransaction approach is correct to pass data to my Fragment, but I don't see what else I need to do to prepare the Fragment to handle the lifecycle events associated with changes in screen orientation, and I don't know how to inflate the Fragment (either implicitly or explicitly.)
(I'm using a tablet, so I have the twoPane display, in case that makes a difference.)
Try moving your setContentView() to BEFORE you start instantiating your fragments.
If you setup your fragments first and try to attach them to views that don't yet exist then errors are likely.
Also ensure that the view you attach your fragments to exists in both your portrait and landscape layouts.
As you are adding your fragments programmatically, ensure you do not add them in your XML layouts as well. You should only have a FrameLayout in your layout to attach your fragment to.
What happens during an orientation change is that the current activity is destroyed and recreated - this is useful, because alternate layouts can be inflated (if you have another one for a different orientation in your resources folder).
Likely what's happening here is that you're trying to access a fragment before it's been inflated (as Kuffs said).
Your fragment needs a public empty constructor.
From http://developer.android.com/reference/android/app/Fragment.html:
All subclasses of Fragment must include a public empty constructor. The framework will often re-instantiate a fragment class when needed, in particular during state restore, and needs to be able to find this constructor to instantiate it. If the empty constructor is not available, a runtime exception will occur in some cases during state restore.

How to manager Fragment like ActivityGroup manager Activity

As we know, ActivityGroup is deprecated.I'm try to reconfigure my code.
this code use ActivityGroup :
public void lauchContentActivity(Intnet intent) {
View view = getLocationActivityManager().startActivity(
intent.getComponent().getShortClassName(),
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP))
.getDecorView();
setContentView(view);
}
So I can toogle any activity's visible ,and save the activity instance state which is hide.
But FragmentManager has only a backstack, and can't bring a fragment to front expect pressing the Back button.
How to manager Fragment like ActivityGroup manager Activity?
I achieved fragment tabs, Its was challenging for me to achieve and to understand fragment hierarchy while adding and removing fragment.
As the question of managing fragments, its depend on your requirement, this sample details you hierarchy of fragments & way to manage fragment with the help of HashMap.
Below class will explain you behaviorof fragment. (class present in that sample)
AppMainTabActivity.java
public class AppMainTabActivity extends FragmentActivity {
/* Your Tab host */
private TabHost mTabHost;
/* A HashMap of stacks, where we use tab identifier as keys.. */
private HashMap<String, Stack<Fragment>> mStacks;
/* Save current tabs identifier in this.. */
private String mCurrentTab;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.app_main_tab_fragment_layout);
/*
* Navigation stacks for each tab gets created.. tab identifier is used
* as key to get respective stack for each tab
*/
mStacks = new HashMap<String, Stack<Fragment>>();
mStacks.put(AppConstants.TAB_A, new Stack<Fragment>());
mStacks.put(AppConstants.TAB_B, new Stack<Fragment>());
mStacks.put(AppConstants.TAB_C, new Stack<Fragment>());
mTabHost = (TabHost) findViewById(android.R.id.tabhost);
mTabHost.setOnTabChangedListener(listener);
mTabHost.setup();
initializeTabs();
}
private View createTabView(final int id) {
View view = LayoutInflater.from(this).inflate(R.layout.tabs_icon, null);
ImageView imageView = (ImageView) view.findViewById(R.id.tab_icon);
imageView.setImageDrawable(getResources().getDrawable(id));
return view;
}
public void initializeTabs() {
/* Setup your tab icons and content views.. Nothing special in this.. */
TabHost.TabSpec spec = mTabHost.newTabSpec(AppConstants.TAB_A);
mTabHost.setCurrentTab(-3);
spec.setContent(new TabHost.TabContentFactory() {
public View createTabContent(String tag) {
return findViewById(R.id.realtabcontent);
}
});
spec.setIndicator(createTabView(R.drawable.toolkittabicon));
mTabHost.addTab(spec);
spec = mTabHost.newTabSpec(AppConstants.TAB_B);
spec.setContent(new TabHost.TabContentFactory() {
public View createTabContent(String tag) {
return findViewById(R.id.realtabcontent);
}
});
spec.setIndicator(createTabView(R.drawable.followtabicon));
mTabHost.addTab(spec);
spec = mTabHost.newTabSpec(AppConstants.TAB_C);
spec.setContent(new TabHost.TabContentFactory() {
public View createTabContent(String tag) {
return findViewById(R.id.realtabcontent);
}
});
spec.setIndicator(createTabView(R.drawable.myhuddletabicion));
mTabHost.addTab(spec);
}
/* Comes here when user switch tab, or we do programmatically */
TabHost.OnTabChangeListener listener = new TabHost.OnTabChangeListener() {
public void onTabChanged(String tabId) {
/* Set current tab.. */
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(AppConstants.TAB_A)) {
pushFragments(tabId, new ToolKitFragment(), false, true);
} else if (tabId.equals(AppConstants.TAB_B)) {
pushFragments(tabId, new FollowFragment(), false, true);
} else if (tabId.equals(AppConstants.TAB_C)) {
pushFragments(tabId, new HuddleFragment(), false, 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,
false);
}
}
};
/*
* Might be useful if we want to switch tab programmatically, from inside
* any of the fragment.
*/
public void setCurrentTab(int val) {
mTabHost.setCurrentTab(val);
}
/*
* To add fragment to a tab. tag -> Tab identifier fragment -> Fragment to
* show, in tab identified by tag shouldAnimate -> should animate
* transaction. false when we switch tabs, or adding first fragment to a tab
* true when when we are pushing more fragment into navigation stack.
* shouldAdd -> Should add to fragment navigation stack (mStacks.get(tag)).
* false when we are switching tabs (except for the first time) true in all
* other cases.
*/
public void pushFragments(String tag, Fragment fragment,
boolean shouldAnimate, boolean shouldAdd) {
if (shouldAdd)
mStacks.get(tag).push(fragment);
FragmentManager manager = getSupportFragmentManager();
FragmentTransaction ft = manager.beginTransaction();
if (shouldAnimate)
ft.setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left);
ft.replace(R.id.realtabcontent, 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.setCustomAnimations(R.anim.slide_in_left, R.anim.slide_out_right);
ft.replace(R.id.realtabcontent, fragment);
ft.commit();
}
#Override
public void onBackPressed() {
if (((BaseFragment) mStacks.get(mCurrentTab).lastElement())
.onBackPressed() == false) {
Log.d("######", "on back press");
/*
* top fragment in current tab doesn't handles back press, we can do
* our thing, which is
*
* if current tab has only one fragment in stack, ie first fragment
* is showing for this tab. finish the activity else pop to previous
* fragment in stack for the same tab
*/
if (mStacks.get(mCurrentTab).size() == 1) {
super.onBackPressed(); // or call finish..
} else {
popFragments();
}
} else {
// do nothing.. fragment already handled back button press.
}
}
/*
* Imagine if you wanted to get an image selected using ImagePicker intent
* to the fragment. Ofcourse I could have created a public function in that
* fragment, and called it from the activity. But couldn't resist myself.
*/
#Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (mStacks.get(mCurrentTab).size() == 0) {
return;
}
/* Now current fragment on screen gets onActivityResult callback.. */
mStacks.get(mCurrentTab).lastElement()
.onActivityResult(requestCode, resultCode, data);
}
}
But if you have child fragment then it will create issue on back press which is not handle in that sample, solution# you have to Override onDetach() method and manage child fragment check below code snippet.
#Override
public void onDetach() {
super.onDetach();
try {
Field childFragmentManager = Fragment.class.getDeclaredField("mChildFragmentManager");
childFragmentManager.setAccessible(true);
childFragmentManager.set(this, null);
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
Additionally refer developer site and dig fragment in details.
The feasibility of this answer will depend on what you want to accomplish with the different activities. I had a similar problem and I solved it by using Fragments. Imagine that you have one Activity for your task which has different facets. Then you can easily use the Activity to gather and persist data that you will need in each Fragment or to feed your business logic and your Fragments can each cater to different facets of the task. I would highly recommend this as Fragmentand FragmentManagerare supposed to replace the deprecated ActivityGroup. Here is some documentation on the matter:
FragmentManager
Fragment
Now, using Fragments is a bit different from using Activity but not to much. Basically, you declare your Fragmentlike any other object. To show your Fragment you will use:
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
YourFragment yourFragment = new YourFragment();
fragmentTransaction.replace(R.id.containerID, yourFragment);
fragmentTransaction.addToBackStack();
fragmentTransaction.commit();
The documentation that you can download using ADK also contains a lot of samples that use Fragments. I believe it is a good start if you want to get some quality code snippets!

Design a HorizontalScrollView inside of ViewPager or use fragments:

I need to design the following screen, and I need your advice:
Explanation:
The title is static/fixed and I don't need to do anything with it.
Yellow: this is the interesting part, I need to design a ViewPager like screen that has the capability to scroll left/right for Max 4 screens.
Red: In every screen I need to add a Table/Grid that can be scrollable as well if it's not fits screen size.
Green: The page switching can be done using the green buttons in the bottom of the screen or by scrolling the ViewPager.
The Question Is: Can this behavior be achieve using a ViewPager or should I use Fragments? If Fragment is the way to go, then how would I implement the page switching using the sliding gesture? if it's a ViewPager then how to add the inside scrolling and how to control it using the buttons at the bottom?
Any help would be appreciated, Thanks.
I think this should be tackled with a Non Swipeable ViewPager. There is no way the view pager and the underlying Fragments should respond to the swiping gesture. The methods to override to disable swiping within the ViewPager are:
onTouchEvent() - returns false.
onInterceptTouchEvent()- returns false.
Refer to this SO question for more information on how to achieve this.
Next up you want to be using Fragments within each of your pager holders. So we're building the following layout:
Within the parent activity a FragmentPagerAdapter is instantiated and your tabs added with a tag:
Activity changes
#Override
protected void onCreate(final Bundle saveInstanceState) {
final FragmentPagerAdapter myTabAdapter = new MyFragmentPagerAdapter(
<Your ViewPager View>, <Your activity context, this>);
myTabAdapter.addTab(getActionBar().newTab(), "YOUR TAG", "Your Title");
// etc...
}
So this gives us the frame of the diagram above. A hosting activity, containing a ViewPager and the underlying tabs. Next up is getting the Fragments (containing your tables) into each of the respective tabs. This is handled by the FragmentPagerAdapter implementation:
Fragment adapter (inner class to activity):
private class MyFragmentPagerAdapter extends FragmentPagerAdapter implements
ActionBar.TabListener, ViewPager.OnPageChangeListener {
/**
* Constructs a pager adapter to back a {#link ViewPager}.
*
* #param pager
* The {#link ViewPager} widget.
* #param activityContext
* The context the widget is being added under.
*/
public SpotMenuFragmentPagerAdapter(final ViewPager pager,
final Context activityContext) {
super(getFragmentManager());
pager.setAdapter(this);
this.context = activityContext;
}
/**
* Adds a tab to the hosting activity action bar.
*
* #param newTab
* The tab to add.
* #param tag
* The tab tag for id purposes.
* #param label
* The label of the tab displayed to the user.
*/
public void addTab(final ActionBar.Tab newTab, final String tag,
final String label) {
newTab.setTag(tag);
newTab.setText(label);
newTab.setTabListener(this);
getSupportActionBar().addTab(newTab);
}
/**
* This is where you do the work of building the correct fragment based
* on the tab currently selected.
*
* #see FragmentPagerAdapter#getItem(int)
*/
#Override
public Fragment getItem(final int position) {
final Tab tab = getActionBar().getTabAt(position);
if ("MY TAG".equals(tab.getTag().toString()) {
// instantiate the fragment (table) for "MY TAG"
} else {
// instantiate something else...
}
}
/**
* One fragment per tab.
*
* #see android.support.v4.view.PagerAdapter#getCount()
*/
#Override
public int getCount() {
return getSupportActionBar().getTabCount();
}
/**
* #see ViewPager.OnPageChangeListener#onPageScrollStateChanged(int)
*/
#Override
public void onPageScrollStateChanged(final int arg0) {
// No-op.
}
/**
* #see ViewPager.OnPageChangeListener#onPageScrolled(int, float, int)
*/
#Override
public void onPageScrolled(final int arg0, final float arg1,
final int arg2) {
// No-op.
}
/**
* #see ViewPager.OnPageChangeListener#onPageSelected(int)
*/
#Override
public void onPageSelected(final int position) {
getSupportActionBar().setSelectedNavigationItem(position);
}
/**
* #see TabListener#onTabSelected(app.ActionBar.Tab,
* app.FragmentTransaction)
*/
#Override
public void onTabSelected(final Tab tab, final FragmentTransaction ft) {
viewPager.setCurrentItem(tab.getPosition());
}
/**
* #see TabListener#onTabUnselected(ActionBar.Tab,
* app.FragmentTransaction)
*/
#Override
public void onTabUnselected(final Tab tab, final FragmentTransaction ft) {
// No-op.
}
/**
* #see TabListener#onTabReselected(ActionBar.Tab,app.FragmentTransaction)
*/
#Override
public void onTabReselected(final Tab tab, final FragmentTransaction ft) {
// No-op.
}
}
So hopefully by this point we have an activity hosting a 'non-swipeable' view pager and a mechanism for switching tabs in the form of the tab bar underneath the title (or alongside depending on the screen size). From this point I am sure you could customise to replace the tab bar with some navigational arrows.
Note: A lot of that was written from memory but hopefully I've conveyed the gist of where I would go with this.
Update
In response to the updated question: you can set the tab to be any old view. Set the TabSpec accordingly. Apologies I haven't used this myself.
You have to use HorizontalScrollView which it'll contain a LinearLayout and the Linear is the one that will contain your individual elements
The XML would see something like this
<HorizontalScrollView
android:id="#+id/horizontal_scroll_parent"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="horizontal"
android:scrollbarStyle="outsideInset">
<LinearLayout android:id="#+id/content_scroll"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="horizontal"
android:gravity="center_vertical" />
</HorizontalScrollView>

Separate Back Stack for each tab in Android using Fragments

I'm trying to implement tabs for navigation in an Android app. Since TabActivity and ActivityGroup are deprecated I would like to implement it using Fragments instead.
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?
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.
Also, it is important that the state for each tab is maintained when the device is rotated.
BR
Martin
Read this before using this solution
Wow, I still can't believe this answer is the one with most votes in this thread. Please don't blindly follow this implementation. I wrote this solution in 2012 (when I was just a novice in Android). Ten years down the line, I can see there is a terrible issue with this solution.
I am storing hard reference to fragments to implement the navigation stack. It is a terrible practice and would result in memory leak. Let the FragmentManager saves the reference to fragments. Just store the fragment identifier if needed.
My answer can be used with above modification if needed. But I don't think we need to write a multi stacked navigation implementation from scratch. There is surely a much better readymade solution for this. I am not much into Android nowadays, so can't point to any.
I am keeping the original answer for the sake of completeness.
Original answer
I am terribly late to this question . But since this thread has been very informative and helpful to me I thought I better post my two pence here.
I needed a screen flow like this (A minimalistic design with 2 tabs and 2 views in each tab),
tabA
-> ScreenA1, ScreenA2
tabB
-> ScreenB1, ScreenB2
I had the same requirements in the past, and I did it using TabActivityGroup (which was deprecated at that time too) and Activities. This time I wanted to use Fragments.
So this is how I done it.
1. Create a base Fragment Class
public class BaseFragment extends Fragment {
AppMainTabActivity mActivity;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mActivity = (AppMainTabActivity) this.getActivity();
}
public void onBackPressed(){
}
public void onActivityResult(int requestCode, int resultCode, Intent data){
}
}
All fragments in your app can extend this Base class. If you want to use special fragments like ListFragment you should create a base class for that too. You will be clear about the usage of onBackPressed() and onActivityResult() if you read the post in full..
2. Create some Tab identifiers, accessible everywhere in project
public class AppConstants{
public static final String TAB_A = "tab_a_identifier";
public static final String TAB_B = "tab_b_identifier";
//Your other constants, if you have them..
}
nothing to explain here..
3. Ok, Main Tab Activity- Please go through comments in code..
public class AppMainFragmentActivity extends FragmentActivity{
/* Your Tab host */
private TabHost mTabHost;
/* A HashMap of stacks, where we use tab identifier as keys..*/
private HashMap<String, Stack<Fragment>> mStacks;
/*Save current tabs identifier in this..*/
private String mCurrentTab;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.app_main_tab_fragment_layout);
/*
* Navigation stacks for each tab gets created..
* tab identifier is used as key to get respective stack for each tab
*/
mStacks = new HashMap<String, Stack<Fragment>>();
mStacks.put(AppConstants.TAB_A, new Stack<Fragment>());
mStacks.put(AppConstants.TAB_B, new Stack<Fragment>());
mTabHost = (TabHost)findViewById(android.R.id.tabhost);
mTabHost.setOnTabChangedListener(listener);
mTabHost.setup();
initializeTabs();
}
private View createTabView(final int id) {
View view = LayoutInflater.from(this).inflate(R.layout.tabs_icon, null);
ImageView imageView = (ImageView) view.findViewById(R.id.tab_icon);
imageView.setImageDrawable(getResources().getDrawable(id));
return view;
}
public void initializeTabs(){
/* Setup your tab icons and content views.. Nothing special in this..*/
TabHost.TabSpec spec = mTabHost.newTabSpec(AppConstants.TAB_A);
mTabHost.setCurrentTab(-3);
spec.setContent(new TabHost.TabContentFactory() {
public View createTabContent(String tag) {
return findViewById(R.id.realtabcontent);
}
});
spec.setIndicator(createTabView(R.drawable.tab_home_state_btn));
mTabHost.addTab(spec);
spec = mTabHost.newTabSpec(AppConstants.TAB_B);
spec.setContent(new TabHost.TabContentFactory() {
public View createTabContent(String tag) {
return findViewById(R.id.realtabcontent);
}
});
spec.setIndicator(createTabView(R.drawable.tab_status_state_btn));
mTabHost.addTab(spec);
}
/*Comes here when user switch tab, or we do programmatically*/
TabHost.OnTabChangeListener listener = new TabHost.OnTabChangeListener() {
public void onTabChanged(String tabId) {
/*Set current tab..*/
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(AppConstants.TAB_A)){
pushFragments(tabId, new AppTabAFirstFragment(), false,true);
}else if(tabId.equals(AppConstants.TAB_B)){
pushFragments(tabId, new AppTabBFirstFragment(), false,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,false);
}
}
};
/* Might be useful if we want to switch tab programmatically, from inside any of the fragment.*/
public void setCurrentTab(int val){
mTabHost.setCurrentTab(val);
}
/*
* To add fragment to a tab.
* tag -> Tab identifier
* fragment -> Fragment to show, in tab identified by tag
* shouldAnimate -> should animate transaction. false when we switch tabs, or adding first fragment to a tab
* true when when we are pushing more fragment into navigation stack.
* shouldAdd -> Should add to fragment navigation stack (mStacks.get(tag)). false when we are switching tabs (except for the first time)
* true in all other cases.
*/
public void pushFragments(String tag, Fragment fragment,boolean shouldAnimate, boolean shouldAdd){
if(shouldAdd)
mStacks.get(tag).push(fragment);
FragmentManager manager = getSupportFragmentManager();
FragmentTransaction ft = manager.beginTransaction();
if(shouldAnimate)
ft.setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left);
ft.replace(R.id.realtabcontent, 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.setCustomAnimations(R.anim.slide_in_left, R.anim.slide_out_right);
ft.replace(R.id.realtabcontent, 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;
}
/* Each fragment represent a screen in application (at least in my requirement, just like an activity used to represent a screen). So if I want to do any particular action
* when back button is pressed, I can do that inside the fragment itself. For this I used AppBaseFragment, so that each fragment can override onBackPressed() or onActivityResult()
* kind of events, and activity can pass it to them. Make sure just do your non navigation (popping) logic in fragment, since popping of fragment is done here itself.
*/
((AppBaseFragment)mStacks.get(mCurrentTab).lastElement()).onBackPressed();
/* Goto previous fragment in navigation stack of this tab */
popFragments();
}
/*
* Imagine if you wanted to get an image selected using ImagePicker intent to the fragment. Ofcourse I could have created a public function
* in that fragment, and called it from the activity. But couldn't resist myself.
*/
#Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if(mStacks.get(mCurrentTab).size() == 0){
return;
}
/*Now current fragment on screen gets onActivityResult callback..*/
mStacks.get(mCurrentTab).lastElement().onActivityResult(requestCode, resultCode, data);
}
}
4. app_main_tab_fragment_layout.xml (In case anyone interested.)
<?xml version="1.0" encoding="utf-8"?>
<TabHost
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#android:id/tabhost"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<LinearLayout
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<FrameLayout
android:id="#android:id/tabcontent"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="0"/>
<FrameLayout
android:id="#+android:id/realtabcontent"
android:layout_width="fill_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<TabWidget
android:id="#android:id/tabs"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="0"/>
</LinearLayout>
</TabHost>
5. AppTabAFirstFragment.java (First fragment in Tab A, simliar for all Tabs)
public class AppTabAFragment extends BaseFragment {
private Button mGotoButton;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_one_layout, container, false);
mGoToButton = (Button) view.findViewById(R.id.goto_button);
mGoToButton.setOnClickListener(listener);
return view;
}
private OnClickListener listener = new View.OnClickListener(){
#Override
public void onClick(View v){
/* Go to next fragment in navigation stack*/
mActivity.pushFragments(AppConstants.TAB_A, new AppTabAFragment2(),true,true);
}
}
}
This might not be the most polished and correct way. But it worked beautifully in my case. Also I only had this requirement in portrait mode. I never had to use this code in a project supporting both orientation. So can't say what kind of challenges I face there..
If anyone want a full project, I have pushed a sample project to github.
We had to implement exactly that same behaviour that you describe for an app recently. The screens and overall flow of the application were already defined so we had to stick with it (it's an iOS app clone...). Luckily, we managed to get rid of the on-screen back buttons :)
We hacked the solution using a mixture of TabActivity, FragmentActivities (we were using the support library for fragments) and Fragments. In retrospective, I'm pretty sure it wasn't the best architecture decision, but we managed to get the thing working. If I had to do it again, I'd probably try to do a more activity-based solution (no fragments), or try and have only one Activity for the tabs and let all the rest be views (which I find are much more reusable than activities overall).
So the requirements were to have some tabs and nestable screens in each tab:
tab 1
screen 1 -> screen 2 -> screen 3
tab 2
screen 4
tab 3
screen 5 -> 6
etc...
So say: user starts in tab 1, navigates from screen 1 to screen 2 then to screen 3, he then switches to tab 3 and navigates from screen 4 to 6; if the switched back to tab 1, he should see screen 3 again and if he pressed Back he should return to screen 2; Back again and he is in screen 1; switch to tab 3 and he's in screen 6 again.
The main Activity in the application is MainTabActivity, which extends TabActivity. Each tab is associated with an activity, lets say ActivityInTab1, 2 and 3. And then each screen will be a fragment:
MainTabActivity
ActivityInTab1
Fragment1 -> Fragment2 -> Fragment3
ActivityInTab2
Fragment4
ActivityInTab3
Fragment5 -> Fragment6
Each ActivityInTab holds only one fragment at a time, and knows how to replace one fragment for another one (pretty much the same as an ActvityGroup). The cool thing is that it's quite easy to mantain separate back stacks for each tab this way.
The functionality for each ActivityInTab was quite the same: know how to navigate from one fragment to another and maintain a back stack, so we put that in a base class. Let's call it simply ActivityInTab:
abstract class ActivityInTab extends FragmentActivity { // FragmentActivity is just Activity for the support library.
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_in_tab);
}
/**
* Navigates to a new fragment, which is added in the fragment container
* view.
*
* #param newFragment
*/
protected void navigateTo(Fragment newFragment) {
FragmentManager manager = getSupportFragmentManager();
FragmentTransaction ft = manager.beginTransaction();
ft.replace(R.id.content, newFragment);
// Add this transaction to the back stack, so when the user presses back,
// it rollbacks.
ft.addToBackStack(null);
ft.commit();
}
}
The activity_in_tab.xml is just this:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/content"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:isScrollContainer="true">
</RelativeLayout>
As you can see, the view layout for each tab was the same. That's because it's just a FrameLayout called content that will hold each fragment. The fragments are the ones that have each screen's view.
Just for the bonus points, we also added some little code to show a confirm dialog when the user presses Back and there are no more fragments to go back to:
// In ActivityInTab.java...
#Override
public void onBackPressed() {
FragmentManager manager = getSupportFragmentManager();
if (manager.getBackStackEntryCount() > 0) {
// If there are back-stack entries, leave the FragmentActivity
// implementation take care of them.
super.onBackPressed();
} else {
// Otherwise, ask user if he wants to leave :)
showExitDialog();
}
}
That's pretty much the setup. As you can see, each FragmentActivity (or just simply Activity in Android >3) is taking care of all the back-stacking with it's own FragmentManager.
An activity like ActivityInTab1 will be really simple, it'll just show it's first fragment (i.e. screen):
public class ActivityInTab1 extends ActivityInTab {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
navigateTo(new Fragment1());
}
}
Then, if a fragment needs to navigate to another fragment, it has to do a little nasty casting... but it's not that bad:
// In Fragment1.java for example...
// Need to navigate to Fragment2.
((ActivityIntab) getActivity()).navigateTo(new Fragment2());
So that's pretty much it. I'm pretty sure this is not a very canonical (and mostly sure not very good) solution, so I'd like to ask seasoned Android developers what would be a better approach to acheive this functionality, and if this is not "how it's done" in Android, I'd appreciate if you could point me to some link or material that explains which is the Android way to approach this (tabs, nested screens in tabs, etc). Feel free to tear apart this answer in the comments :)
As a sign that this solution is not very good is that recently I had to add some navigation functionality to the application. Some bizarre button that should take the user from one tab into another and into a nested screen. Doing that programmatically was a pain in the butt, because of who-knows-who problems and dealing with when are fragments and activities actually instantiated and initialized. I think it would have been much easier if those screens and tabs were all just Views really.
Finally, if you need to survive orientation changes, it's important that your fragments are created using setArguments/getArguments. If you set instance variables in your fragments' constructors you'll be screwed. But fortunately that's really easy to fix: just save everything in setArguments in the constructor and then retrieve those things with getArguments in onCreate to use them.
The framework won't currently do this for you automatically. You will need to build and manage your own back stacks for each tab.
To be honest, this seems like a really questionable thing to do. I can't imagine it resulting in a decent UI -- if the back key is going to do different things depending on the tab I am, especially if the back key also has its normal behavior of closing the entire activity when at the top of the stack... sounds nasty.
If you are trying to build something like a web browser UI, to get a UX that is natural to the user is going to involve a lot of subtle tweaks of behavior depending on context, so you'll definitely need to do your own back stack management rather than rely on some default implementation in the framework. For an example try paying attention to how the back key interacts with the standard browser in the various ways you can go in and out of it. (Each "window" in the browser is essentially a tab.)
This can be easily achieved with ChildFragmentManager
Here is post about this with associated project. take a look,
http://tausiq.wordpress.com/2014/06/06/android-multiple-fragments-stack-in-each-viewpager-tab/
Storing strong references to fragments is not the correct way.
FragmentManager provides putFragment(Bundle, String, Fragment) and saveFragmentInstanceState(Fragment).
Either one is enough to implement a backstack.
Using putFragment, instead of replacing a Fragment, you detach the old one and add the new one. This is what the framework does to a replace transaction that is added to the backstack. putFragment stores an index to the current list of active Fragments and those Fragments are saved by the framework during orientation changes.
The second way, using saveFragmentInstanceState, saves the whole fragment state to a Bundle allowing you to really remove it, rather than detaching. Using this approach makes the back stack easier to manipulate, as you can pop a Fragment whenever you want.
I used the second method for this usecase:
SignInFragment ----> SignUpFragment ---> ChooseBTDeviceFragment
\ /
\------------------------/
I don't want the user to return to the Sign Up screen, from the third one, by pressing the back button. I also do flip animations between them (using onCreateAnimation), so hacky solutions won't work, atleast without the user clearly noticing something is not right.
This is a valid use case for a custom backstack, doing what the user expects...
private static final String STATE_BACKSTACK = "SetupActivity.STATE_BACKSTACK";
private MyBackStack mBackStack;
#Override
protected void onCreate(Bundle state) {
super.onCreate(state);
if (state == null) {
mBackStack = new MyBackStack();
FragmentManager fm = getSupportFragmentManager();
FragmentTransaction tr = fm.beginTransaction();
tr.add(R.id.act_base_frg_container, new SignInFragment());
tr.commit();
} else {
mBackStack = state.getParcelable(STATE_BACKSTACK);
}
}
#Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelable(STATE_BACKSTACK, mBackStack);
}
private void showFragment(Fragment frg, boolean addOldToBackStack) {
final FragmentManager fm = getSupportFragmentManager();
final Fragment oldFrg = fm.findFragmentById(R.id.act_base_frg_container);
FragmentTransaction tr = fm.beginTransaction();
tr.replace(R.id.act_base_frg_container, frg);
// This is async, the fragment will only be removed after this returns
tr.commit();
if (addOldToBackStack) {
mBackStack.push(fm, oldFrg);
}
}
#Override
public void onBackPressed() {
MyBackStackEntry entry;
if ((entry = mBackStack.pop()) != null) {
Fragment frg = entry.recreate(this);
FragmentManager fm = getSupportFragmentManager();
FragmentTransaction tr = fm.beginTransaction();
tr.replace(R.id.act_base_frg_container, frg);
tr.commit();
// Pop it now, like the framework implementation.
fm.executePendingTransactions();
} else {
super.onBackPressed();
}
}
public class MyBackStack implements Parcelable {
private final List<MyBackStackEntry> mList;
public MyBackStack() {
mList = new ArrayList<MyBackStackEntry>(4);
}
public void push(FragmentManager fm, Fragment frg) {
push(MyBackStackEntry.newEntry(fm, frg);
}
public void push(MyBackStackEntry entry) {
if (entry == null) {
throw new NullPointerException();
}
mList.add(entry);
}
public MyBackStackEntry pop() {
int idx = mList.size() - 1;
return (idx != -1) ? mList.remove(idx) : null;
}
#Override
public int describeContents() {
return 0;
}
#Override
public void writeToParcel(Parcel dest, int flags) {
final int len = mList.size();
dest.writeInt(len);
for (int i = 0; i < len; i++) {
// MyBackStackEntry's class is final, theres no
// need to use writeParcelable
mList.get(i).writeToParcel(dest, flags);
}
}
protected MyBackStack(Parcel in) {
int len = in.readInt();
List<MyBackStackEntry> list = new ArrayList<MyBackStackEntry>(len);
for (int i = 0; i < len; i++) {
list.add(MyBackStackEntry.CREATOR.createFromParcel(in));
}
mList = list;
}
public static final Parcelable.Creator<MyBackStack> CREATOR =
new Parcelable.Creator<MyBackStack>() {
#Override
public MyBackStack createFromParcel(Parcel in) {
return new MyBackStack(in);
}
#Override
public MyBackStack[] newArray(int size) {
return new MyBackStack[size];
}
};
}
public final class MyBackStackEntry implements Parcelable {
public final String fname;
public final Fragment.SavedState state;
public final Bundle arguments;
public MyBackStackEntry(String clazz,
Fragment.SavedState state,
Bundle args) {
this.fname = clazz;
this.state = state;
this.arguments = args;
}
public static MyBackStackEntry newEntry(FragmentManager fm, Fragment frg) {
final Fragment.SavedState state = fm.saveFragmentInstanceState(frg);
final String name = frg.getClass().getName();
final Bundle args = frg.getArguments();
return new MyBackStackEntry(name, state, args);
}
public Fragment recreate(Context ctx) {
Fragment frg = Fragment.instantiate(ctx, fname);
frg.setInitialSavedState(state);
frg.setArguments(arguments);
return frg;
}
#Override
public int describeContents() {
return 0;
}
#Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(fname);
dest.writeBundle(arguments);
if (state == null) {
dest.writeInt(-1);
} else if (state.getClass() == Fragment.SavedState.class) {
dest.writeInt(0);
state.writeToParcel(dest, flags);
} else {
dest.writeInt(1);
dest.writeParcelable(state, flags);
}
}
protected MyBackStackEntry(Parcel in) {
final ClassLoader loader = getClass().getClassLoader();
fname = in.readString();
arguments = in.readBundle(loader);
switch (in.readInt()) {
case -1:
state = null;
break;
case 0:
state = Fragment.SavedState.CREATOR.createFromParcel(in);
break;
case 1:
state = in.readParcelable(loader);
break;
default:
throw new IllegalStateException();
}
}
public static final Parcelable.Creator<MyBackStackEntry> CREATOR =
new Parcelable.Creator<MyBackStackEntry>() {
#Override
public MyBackStackEntry createFromParcel(Parcel in) {
return new MyBackStackEntry(in);
}
#Override
public MyBackStackEntry[] newArray(int size) {
return new MyBackStackEntry[size];
}
};
}
Disclaimer:
I feel this is the best place to post a related solution I have worked on for a similar type of problem that seems to be pretty standard Android stuff. It's not going to solve the problem for everyone, but it may help some.
If the primary difference between your fragments is only the data backing them up (ie, not a lot of big layout differences), then you may not need to actually replace the fragment, but merely swap out the underlying data and refresh the view.
Here's a description of one possible example for this approach:
I have an app that uses ListViews. Each item in the list is a parent with some number of children. When you tap the item, a new list needs to open with those children, within the same ActionBar tab as the original list. These nested lists have a very similar layout (some conditional tweaks here and there perhaps), but the data is different.
This app has several layers of offspring beneath the initial parent list and we may or may not have data from the server by the time a user attempts to access any certain depth beyond the first. Because the list is constructed from a database cursor, and the fragments use a cursor loader and cursor adapter to populate the list view with list items, all that needs to happen when a click is registered is:
1) Create a new adapter with the appropriate 'to' and 'from' fields that will match new item views being added to the list and the columns returned by the new cursor.
2) Set this adapter as the new adapter for the ListView.
3) Build a new URI based on the item that was clicked and restart the cursor loader with the new URI (and projection). In this example, the URI is mapped to specific queries with the selection args passed down from the UI.
4) When the new data has been loaded from the URI, swap the cursor associated with the adapter to the new cursor, and the list will then refresh.
There is no backstack associated with this since we aren't using transactions, so you will have to either build your own, or play the queries in reverse when backing out of the hierarchy. When I tried this, the queries were fast enough that I just perform them again in oNBackPressed() up until I am at the top of hierarchy, at which point the framework takes over the back button again.
If you find yourself in a similar situation, make sure to read the docs:
http://developer.android.com/guide/topics/ui/layout/listview.html
http://developer.android.com/reference/android/support/v4/app/LoaderManager.LoaderCallbacks.html
I hope this helps someone!
I had exactly the same problem and implemented an open source github project that covers stacked tab, back and up navigation and is well tested and documented:
https://github.com/SebastianBaltesObjectCode/PersistentFragmentTabs
This is a simple and small framework for navigation tabs and fragment switching and handling of up and back navigation. Each tab has its own stack of fragments. It uses ActionBarSherlock and is compatible back to API level 8.
This is a complex problem as Android only handles 1 back stack, but this is feasible. It took me days to create a library called Tab Stacker that does exactly what you are looking for: a fragment history for each tab. It is open source and fully documented, and can be included easily with gradle. You can find the library on github: https://github.com/smart-fun/TabStacker
You can also download the sample app to see that the behaviour corresponds to your needs:
https://play.google.com/apps/testing/fr.arnaudguyon.tabstackerapp
If you have any question don't hesitate to drop a mail.
I'd like to suggest my own solution in case somebody is looking and want to try and choose the best one for his/her needs.
https://github.com/drusak/tabactivity
The purpose of creating the library is quite banal - implement it like iPhone.
The main advantages:
use android.support.design library with TabLayout;
each tab has its own stack using FragmentManager (without saving fragments' references);
support for deep linking (when you need to open specific tab and specific fragment's level in it);
saving / restoring states of tabs;
adaptive lifecycle methods of fragments in tabs;
quite easy to implement for your needs.
A simple solution:
Every time you change tab/root view call:
fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
It will clear the BackStack. Remember to call this before you change the root fragment.
And add fragments with this:
FragmentTransaction transaction = getFragmentManager().beginTransaction();
NewsDetailsFragment newsDetailsFragment = NewsDetailsFragment.newInstance(newsId);
transaction.add(R.id.content_frame, newsDetailsFragment).addToBackStack(null).commit();
Note the .addToBackStack(null) and the transaction.add could e.g. be changed with transaction.replace.
This thread was very very interesting and useful.
Thanks Krishnabhadra for your explanation and code, I use your code and improved a bit, allowing to persist the stacks, currentTab, etc... from change configuration (rotating mainly).
Tested on a real 4.0.4 and 2.3.6 devices, not tested on emulator
I change this part of code on "AppMainTabActivity.java", the rest stay the same.
Maybe Krishnabhadra will add this on his code.
Recover data onCreate:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.app_main_tab_fragment_layout);
/*
* Navigation stacks for each tab gets created..
* tab identifier is used as key to get respective stack for each tab
*/
//if we are recreating this activity...
if (savedInstanceState!=null) {
mStacks = (HashMap<String, Stack<Fragment>>) savedInstanceState.get("stack");
mCurrentTab = savedInstanceState.getString("currentTab");
}
else {
mStacks = new HashMap<String, Stack<Fragment>>();
mStacks.put(AppConstants.TAB_A, new Stack<Fragment>());
mStacks.put(AppConstants.TAB_B, new Stack<Fragment>());
}
mTabHost = (TabHost)findViewById(android.R.id.tabhost);
mTabHost.setup();
initializeTabs();
//set the listener the last, to avoid overwrite mCurrentTab everytime we add a new Tab
mTabHost.setOnTabChangedListener(listener);
}
Save the variables and put to Bundle:
//Save variables while recreating
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putSerializable("stack", mStacks);
outState.putString("currentTab", mCurrentTab);
//outState.putInt("tabHost",mTabHost);
}
If exist a previous CurrentTab, set this, else create a new Tab_A:
public void initializeTabs(){
/* Setup your tab icons and content views.. Nothing special in this..*/
TabHost.TabSpec spec = mTabHost.newTabSpec(AppConstants.TAB_A);
spec.setContent(new TabHost.TabContentFactory() {
public View createTabContent(String tag) {
return findViewById(R.id.realtabcontent);
}
});
spec.setIndicator(createTabView(R.drawable.tab_a_state_btn));
mTabHost.addTab(spec);
spec = mTabHost.newTabSpec(AppConstants.TAB_B);
spec.setContent(new TabHost.TabContentFactory() {
public View createTabContent(String tag) {
return findViewById(R.id.realtabcontent);
}
});
spec.setIndicator(createTabView(R.drawable.tab_b_state_btn));
mTabHost.addTab(spec);
//if we have non default Tab as current, change it
if (mCurrentTab!=null) {
mTabHost.setCurrentTabByTag(mCurrentTab);
} else {
mCurrentTab=AppConstants.TAB_A;
pushFragments(AppConstants.TAB_A, new AppTabAFirstFragment(), false,true);
}
}
I hope this helps other people.
I would recommend do not use backstack based on HashMap>
there is lots of bugs in "do not keep activities" mode.
It will not correctly restore the state in case you deeply in fragment's stack.
And also will be crached in nested map fragment (with exeption: Fragment no view found for ID) .
Coz HashMap> after background\foreground app will be null
I optimize code above for work with fragment's backstack
It is bottom TabView
Main activity Class
import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.Window;
import android.widget.ImageView;
import android.widget.TabHost;
import android.widget.TextView;
import com.strikersoft.nida.R;
import com.strikersoft.nida.abstractActivity.BaseActivity;
import com.strikersoft.nida.screens.tags.mapTab.MapContainerFragment;
import com.strikersoft.nida.screens.tags.searchTab.SearchFragment;
import com.strikersoft.nida.screens.tags.settingsTab.SettingsFragment;
public class TagsActivity extends BaseActivity {
public static final String M_CURRENT_TAB = "M_CURRENT_TAB";
private TabHost mTabHost;
private String mCurrentTab;
public static final String TAB_TAGS = "TAB_TAGS";
public static final String TAB_MAP = "TAB_MAP";
public static final String TAB_SETTINGS = "TAB_SETTINGS";
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().requestFeature(Window.FEATURE_ACTION_BAR);
getActionBar().hide();
setContentView(R.layout.tags_activity);
mTabHost = (TabHost) findViewById(android.R.id.tabhost);
mTabHost.setup();
if (savedInstanceState != null) {
mCurrentTab = savedInstanceState.getString(M_CURRENT_TAB);
initializeTabs();
mTabHost.setCurrentTabByTag(mCurrentTab);
/*
when resume state it's important to set listener after initializeTabs
*/
mTabHost.setOnTabChangedListener(listener);
} else {
mTabHost.setOnTabChangedListener(listener);
initializeTabs();
}
}
private View createTabView(final int id, final String text) {
View view = LayoutInflater.from(this).inflate(R.layout.tabs_icon, null);
ImageView imageView = (ImageView) view.findViewById(R.id.tab_icon);
imageView.setImageDrawable(getResources().getDrawable(id));
TextView textView = (TextView) view.findViewById(R.id.tab_text);
textView.setText(text);
return view;
}
/*
create 3 tabs with name and image
and add it to TabHost
*/
public void initializeTabs() {
TabHost.TabSpec spec;
spec = mTabHost.newTabSpec(TAB_TAGS);
spec.setContent(new TabHost.TabContentFactory() {
public View createTabContent(String tag) {
return findViewById(R.id.realtabcontent);
}
});
spec.setIndicator(createTabView(R.drawable.tab_tag_drawable, getString(R.string.tab_tags)));
mTabHost.addTab(spec);
spec = mTabHost.newTabSpec(TAB_MAP);
spec.setContent(new TabHost.TabContentFactory() {
public View createTabContent(String tag) {
return findViewById(R.id.realtabcontent);
}
});
spec.setIndicator(createTabView(R.drawable.tab_map_drawable, getString(R.string.tab_map)));
mTabHost.addTab(spec);
spec = mTabHost.newTabSpec(TAB_SETTINGS);
spec.setContent(new TabHost.TabContentFactory() {
public View createTabContent(String tag) {
return findViewById(R.id.realtabcontent);
}
});
spec.setIndicator(createTabView(R.drawable.tab_settings_drawable, getString(R.string.tab_settings)));
mTabHost.addTab(spec);
}
/*
first time listener will be trigered immediatelly after first: mTabHost.addTab(spec);
for set correct Tab in setmTabHost.setCurrentTabByTag ignore first call of listener
*/
TabHost.OnTabChangeListener listener = new TabHost.OnTabChangeListener() {
public void onTabChanged(String tabId) {
mCurrentTab = tabId;
if (tabId.equals(TAB_TAGS)) {
pushFragments(SearchFragment.getInstance(), false,
false, null);
} else if (tabId.equals(TAB_MAP)) {
pushFragments(MapContainerFragment.getInstance(), false,
false, null);
} else if (tabId.equals(TAB_SETTINGS)) {
pushFragments(SettingsFragment.getInstance(), false,
false, null);
}
}
};
/*
Example of starting nested fragment from another fragment:
Fragment newFragment = ManagerTagFragment.newInstance(tag.getMac());
TagsActivity tAct = (TagsActivity)getActivity();
tAct.pushFragments(newFragment, true, true, null);
*/
public void pushFragments(Fragment fragment,
boolean shouldAnimate, boolean shouldAdd, String tag) {
FragmentManager manager = getFragmentManager();
FragmentTransaction ft = manager.beginTransaction();
if (shouldAnimate) {
ft.setCustomAnimations(R.animator.fragment_slide_left_enter,
R.animator.fragment_slide_left_exit,
R.animator.fragment_slide_right_enter,
R.animator.fragment_slide_right_exit);
}
ft.replace(R.id.realtabcontent, fragment, tag);
if (shouldAdd) {
/*
here you can create named backstack for realize another logic.
ft.addToBackStack("name of your backstack");
*/
ft.addToBackStack(null);
} else {
/*
and remove named backstack:
manager.popBackStack("name of your backstack", FragmentManager.POP_BACK_STACK_INCLUSIVE);
or remove whole:
manager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
*/
manager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
}
ft.commit();
}
/*
If you want to start this activity from another
*/
public static void startUrself(Activity context) {
Intent newActivity = new Intent(context, TagsActivity.class);
newActivity.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(newActivity);
context.finish();
}
#Override
public void onSaveInstanceState(Bundle outState) {
outState.putString(M_CURRENT_TAB, mCurrentTab);
super.onSaveInstanceState(outState);
}
#Override
public void onBackPressed(){
super.onBackPressed();
}
}
tags_activity.xml
<
?xml version="1.0" encoding="utf-8"?>
<TabHost
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#android:id/tabhost"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="#android:id/tabcontent"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="0"/>
<FrameLayout
android:id="#+android:id/realtabcontent"
android:background="#drawable/bg_main_app_gradient"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<TabWidget
android:id="#android:id/tabs"
android:background="#EAE7E1"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="0"/>
</LinearLayout>
</TabHost>
tags_icon.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/tabsLayout"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="#drawable/bg_tab_gradient"
android:gravity="center"
android:orientation="vertical"
tools:ignore="contentDescription" >
<ImageView
android:id="#+id/tab_icon"
android:layout_marginTop="4dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="#+id/tab_text"
android:layout_marginBottom="3dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#color/tab_text_color"/>
</LinearLayout>

Categories

Resources