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>
Related
I am trying to use flip animations in Android for practising vocabulary. I have made a container for my Fragments:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/container"
android:layout_width="match_parent"
android:layout_height="100dp"
android:onClick="flipCard"
/>
and two Fragments: fragment_card_flip.xml and fragment_card_back.xml. One is for showing the word and the other, the meaning on the back of the card.
I am getting a word from database randomly and want to change the text in both Fragments i.e., put the word on the front Fragment and the meaning on the back Fragment. In the onClick() in the Fragment I am calling a flipcard() function:
public void flipCard(View view) {
if (mShowingBack) {
getFragmentManager().popBackStack();
mShowingBack=false;
f = (Fragment) getFragmentManager().findFragmentById(R.id.container);
if(f!=null)
((TextView) f.getView().findViewById(R.id.text)).setText(w);
return;
}
// Flip to the back.
mShowingBack = true;
getFragmentManager().beginTransaction().setCustomAnimations(
R.animator.card_flip_right_in,
R.animator.card_flip_right_out,
R.animator.card_flip_left_in,
R.animator.card_flip_left_out)
.replace(R.id.container, new CardBackFragment())
.addToBackStack(null)
.commit();
}
This is my attempt to change the word only, not the definition. But I can't change the text data of the Fragment dynamically. Please help me to update both my Fragments dynamically with a new word/meaning from the database.
I think the first step is to change your Fragments so that they take the word they will display as an argument:
public class CardFlip extends Fragment {
public static String ARGUMENT_WORD = "ARGUMENT_WORD"
public static CardFlip instantiate(String text) {
CardFlip cardFlip = new CardFlip();
Bundle argsBundle = new Bundle();
argsBundle.putString(ARGUMENT_WORD, text);
cardFlip.setArguments(argsBundle);
return cardFlip;
}
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
String text = getArguments().getString(ARGUMENT_WORD);
((TextView) findViewById(R.id.text)).setText(text);
}
}
Do this for both Fragments. Now you can make fragments dynamically with the words you want.
Now all you need to do is refactor your flipcard() method so that it uses the static instantiate method rather than the new CardFlip() constructor. Since you always want your Fragments to be up-to-date with the latest words, don't use popBackStack() use FragmentManager.beginTransaction().replace(). Something like this:
public void flipCard(View view) {
Fragment frag;
if (mShowingBack) {
frag = CardFlip.instantiate(currentWord);
}
else {
frag = CardBack.instantiate(currentWord);
}
getFragmentManager().beginTransaction()
.replace(R.id.fragment_container, frag)
.commit();
}
Then all you need is some logic for when to change the word randomly.
These are all concepts from the official developer's guide so you study this very carefully: consider reading the guide there multiple times and taking notes.
I am working on an app that has a support Fragment with a SearchView widget and RecyclerView to present search results which sends the user to a FragmentActivity to display the details of the selection. All of this works fine, but I'm seeing inconsistent behavior between the Nexus 6 emulator and the actual device in regards to the backstack. In the emulator, everything works as I would expect, with the user being taken back to the search results fragment if pressing the Back button while on the details FragmentActivity. On the actual Nexus 6 device, the user is taken all the way back to the AppCompatActivity which contains my app's menu (this activity uses the support FragmentManager, which adds the fragments it manages to the backstack):
private void replaceFragment(Fragment supportFragment) {
String fragmentName = supportFragment.getClass().getSimpleName();
supportFragmentManager.beginTransaction()
.addToBackStack(fragmentName)
.replace(R.id.frame_menu_container, supportFragment, fragmentName)
.commit();
}
The code to send the user from the Fragment to the FragmentActivity is just an intent with extras:
Intent intent = new Intent(getActivity(), PlayerDetailsActivity.class);
intent.putExtra(TransientPlayer.BUNDLE_KEY, transientPlayer);
startActivity(intent);
I am not doing anything special with the backstack (yet) - this is with default behavior.
What could be the issue here? I've done some reading on the backstack and I haven't seen anything yet on manually adding something to the backstack when you're going back from a FragmentActivy to a Fragment where you're not using the FragmentManager.
EDIT 1: Here's the layout XML where frame_menu_container is defined:
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:theme="#style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="#style/ThemeOverlay.AppCompat.Light"/>
<FrameLayout
android:id="#+id/frame_menu_container"
android:layout_below="#id/toolbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:layout="#layout/fragment_home">
</FrameLayout>
</RelativeLayout>
EDIT 2: OK, here is a textual description of my high-level activities and fragments:
NavigationMenuActivity, which extends AppCompatActivity. This is where all the fragments are swapped in and out as needed, and the SupportFragmentManager is used and where I have the replaceFragment() method.
I have the following fragments, all of which either display static information, make REST calls to retrieve and display data, or allow the user to send feedback. All extend android.support.v4.app.Fragment, and none of them have other fragments or activities the user can go to.
HomeFragment
CalendarFragment
StandingsFragment
PlayerSearchFragment (see below)
AboutFragment
FeedbackFragment
The only exception in functionality for these fragments is the PlayerSearchFragment, which also extends android.support.v4.app.Fragment. This fragment contains a RecyclerView that displays the results of a REST call when users want to search for players. When results are returned and the user selects an item from the list, they are sent to a PlayerDetailsActivity which extends FragmentActivity.
The PlayerDetailsActivity uses a FragmentTabHost view which contains different types of information about the player that can be viewed. This is the "end of the line" down this path - there are no other fragments or activities the user can go to. This is where the issue is. When I hit the Back button while on this activity, my intention is to have the user go back to the PlayerSearchFragment fragment (search results), which in this case, they do if I'm in a Nexus 6 emulator, but they go all the way back to the NavigationMenuActivity activity if I'm on my actual Nexus 6 device.
I have the following method in the PlayerDetailsActivity which is called when the Back button is pressed, but it always shows zero entries:
#Override
public void onBackPressed() {
FragmentManager fragmentManager = getSupportFragmentManager();
int count = fragmentManager.getBackStackEntryCount();
Log.i(TAG, "Backstack : There are " + count + " entries");
for (int i = 0; i > count; i++) {
FragmentManager.BackStackEntry backStackEntry = fragmentManager.getBackStackEntryAt(i);
Log.i(TAG, String.format("Backstack : id=%d, name=%s, shortTitle=%s, breadcrumbTitle=%s",
backStackEntry.getId(),
backStackEntry.getName(),
backStackEntry.getBreadCrumbShortTitle(),
backStackEntry.getBreadCrumbTitle()));
}
super.onBackPressed();
}
My hope is to have the experience be the same for all devices, whether hardware or emulator.
I think you want addToBackStack(fragmentName) called after replace(R.id.frame_menu_container, supportFragment, fragmentName).
Tell me what happens.
First thing according to your scenario you don't need to add transaction to BackStack as it's used only when you are navigating in between fragments in the same activity.
Now for your another point that when you back press you are not getting your fragment is you click two times. one click removes your second activity and another one is removing the fragment which is added in your backstack. As for your information Fragment dont have backpress callback, they rely on activity onbackPressed() method
I tried to run this code and it is running properly at my Nexus 5 device.
Although I'm attaching code what i am using
public class MainActivity extends FragmentActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
replaceFragment(new FragmentA());
}
private void replaceFragment(Fragment supportFragment) {
getSupportFragmentManager().beginTransaction()
.replace(R.id.frame_menu_container, supportFragment
.commit();
}
}
FragmentA is here:
public class FragmentA extends Fragment {
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_a, null);
view.findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
Intent intent = new Intent(getActivity(), AnotherActivity.class);
startActivity(intent);
}
});
return view;
}
}
And Another Activity is like:
public class AnotherActivity extends Activity {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.another_act);
}
}
I don't think that you should face any problem.
In case you still face problem tell me your device name and attach a code here so that i can test and will let you know.
well try this, hopefully will get you out of this error-hole.
This answer gives you a workaround but not what is really happening
#Override
public void onBackPressed() {
FragmentManager fragmentManager = getSupportFragmentManager();
int count = fragmentManager.getBackStackEntryCount();
Log.i(TAG, "Backstack : There are " + count + " entries");
if(count > 0){
fragmentManager.popBackStack(); //or popBackStackImmediate();
for (int i = 0; i > count; i++) {
FragmentManager.BackStackEntry backStackEntry = fragmentManager.getBackStackEntryAt(i);
Log.i(TAG, String.format("Backstack : id=%d, name=%s, shortTitle=%s, breadcrumbTitle=%s",
backStackEntry.getId(),
backStackEntry.getName(),
backStackEntry.getBreadCrumbShortTitle(),
backStackEntry.getBreadCrumbTitle()));
}
}else{
super.onBackPressed();
}
}
While I still don't have an answer as to why I couldn't get this to work the way I had hoped, I do have a workaround, and it does work well for what I need to do. Basically, I override the Up and Back navigation callbacks in PlayerDetailsActivity and use an intent (with data) to explicitly send the user back to NavigationMenuActivity. The data I send in the intent is basically just a boolean that tells me if I'm coming from the PlayerDetailsActivity and if so, show that fragment rather than the Home fragment, like so:
#Override
public void onBackPressed() {
sendBackToMenu();
}
#Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
sendBackToMenu();
finish();
return true;
}
return false;
}
private void sendBackToMenu() {
Intent backToMenuIntent = new Intent(this, NavigationMenuActivity.class);
backToMenuIntent.putExtra("FROM_PLAYER_DETAILS", true);
startActivity(backToMenuIntent);
}
The above changes were after I changed the class to extend from AppCompatActivity and added the following in PlayerDetailsActivity.onCreate() to activate the ActionBar for Up navigation:
ActionBar actionBar = getSupportActionBar();
assert actionBar != null;
actionBar.setTitle(R.string.player_details);
actionBar.setDisplayHomeAsUpEnabled(true);
Also, in NavigationMenuActivity.onCreate() I do this:
if (getIntent().getBooleanExtra("FROM_PLAYER_DETAILS", false))
replaceFragment(new PlayerSearchFragment());
else
replaceFragment(new HomeFragment());
I don't know if this is the most 'appropriate' way to do this, but it work well and does what I need across a handful of hardware devices and Nexus 4/5/6 emulators.
I have an android application where I use the ActionBar in NAVIGATION_MODE_TABS mode.
Currently, I am heavily using Fragments. So, that worked quite well for the displayed tabs. I am facing a design issue where I think I am going in the wrong direction.
MainActivity has three tabs. One tab in particular have bunch of navigations on it.
MainActivity
Tab1
Tab2
Tab3 (This has buttons that should display different fragments based on what the user clicked)
The requirement is to keep the tabs always visible with the same text. So, I ended up creating bunch of activities that inherit from the MainActivity where I made the third tab content based on what the user clicked on.
This allowed me to sustain the content in a good flow and keep the look consistent. However, It seems an overhead to create an activity.
I tried to leverage the FragmentManager and pushToStack. However, that route didn't seem to work.
I am relying on this interface ActionBar.TabListener to properly attach and detach the fragments based when the user clicks on the tab. That is good. However, the minute I introduce a different fragment that seemed problematic.
Hope That is clear and I am looking for the best advice.
I solved it by relying on the the following logic:
public class FragmentTabListener implements ActionBar.TabListener
{
private String _fragmentClassName;
private Context _context;
private Fragment _fragment;
private Boolean _cleanStack = true;
public FragmentTabListener(Context context, String fragmentClassName,
Boolean cleanStack)
{
_context = context;
_fragmentClassName = fragmentClassName;
_cleanStack = cleanStack;
}
public FragmentTabListener(Context context, String fragmentClassName)
{
this(context, fragmentClassName, false);
}
#Override
public void onTabReselected(Tab tab, FragmentTransaction ft)
{
cleanFragmentManagerStack();
if (this._cleanStack)
{
ft.attach(_fragment);
}
}
private void cleanFragmentManagerStack()
{
if (this._cleanStack)
{
FragmentManager mgr = ((Activity) _context).getFragmentManager();
int backStackCount = mgr.getBackStackEntryCount();
for (int i = 0; i < backStackCount; i++)
{
// Get the back stack fragment id.
int backStackId = mgr.getBackStackEntryAt(i).getId();
mgr.popBackStack(backStackId,
FragmentManager.POP_BACK_STACK_INCLUSIVE);
}
}
}
#Override
public void onTabSelected(Tab tab, FragmentTransaction ft)
{
if (_fragment == null)
{
_fragment = Fragment.instantiate(_context, _fragmentClassName);
ft.add(android.R.id.content, _fragment);
}
else
{
ft.attach(_fragment);
}
}
#Override
public void onTabUnselected(Tab tab, FragmentTransaction ft)
{
this.cleanFragmentManagerStack();
// TODO Auto-generated method stub
ft.detach(_fragment);
}
}
Any action on my original fragment, I was adding a fragment to the current stack.
This worked. Hope it helps someone.
From what I understand you need 3 fragments, each one of them in one tab, and in the 3rd tab you can have, inside that fragment, based on what user clicked, a different fragment inside (nested fragments are supported now). Maybe I didn't understand your problem correctly, if that's the case tell me.
Cheers!
I'm trying to make a simple reminder app. When creating a new reminder I have the following setup:
MainEditActivity.java: 2 placeholder FrameLayouts for the following fragments:
EditNameFragment.java
custom Action Bar (Cancel, OK button)
EditText for the reminder name
CheckBox to toggle one of the following fragments
EditDateFragment.java OR EditLocationFragment.java (both have a lot of views)
MainEditActivity.java:
private LocationFragment mLocationFragment;
private DateFragment mDateFragment;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_new_remainer_main); // 2 frame layouts
if (savedInstanceState == null) {
Fragment newFragment = new NewReminderFragment();
FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.add(R.id.reminder_title_fragment, newFragment).commit();
mDateFragment = new DateFragment();
ft = getFragmentManager().beginTransaction();
ft.add(R.id.date_or_location_fragment, mDateFragment).commit();
}
}
public void onCheckBoxClick(View view)
{
FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
if(mLocationFragment == null)
mLocationFragment = new LocationFragment();
CheckBox checkBox = (CheckBox)findViewById(R.id.checkBoxID);
if(checkBox != null)
{
if(checkBox.isChecked()) {
fragmentTransaction.replace(R.id.date_or_location_fragment, mLocationFragment);
} else {
fragmentTransaction.replace(R.id.date_or_location_fragment, mDateFragment);
}
fragmentTransaction.commit();
}
#Override
public void handleEvent(EventInfo event) {
// here I get all the data (name, data, time, location, etc)
}
EditNameFragment.java:
private EventHandler mEventHandler;
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
mEventHandler = (EventHandler)activity;
}
View doneButton = actionBarButtons.findViewById(R.id.doneBtnTextID);
doneButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
createNewReminder();
}
});
private void createNewReminder()
{
// Event info object to store all information (reminder name, data, time, etc)
EventInfo eventInfo = new EventInfo();
// access either Date or Location Fragment (depending on which is visible)
// FIXME: access the fragments and get the data
// let the activity know to display the new reminder
mEventHandler.handleEvent(eventInfo);
}
Question: I started off using only 1 MainActivity with all elements and a ViewFlipper. Then I read that fragments are better to eventually adjust to tablets better. Is this the right way to lay this out?
Question: I know how to get the data from EditNameFragment.java. But how do I get the data that the user entered from EditDataFragment or EditLocationFragment?
Do I need an instance of those two Fragments in the MainActivity to access their Views? Or do I need another callback like I did with EventHandler? If yes, would the rule be to have a callback for each fragment?
I guess I'm struggling a bit with the communication flow between those components. And I know fragment to fragment communication isn't a good design
Fragments are self-contained, modular components that would make sense if presented by themselves independently of the other. The canonical example is a list of newspaper articles and the display for an article...both could be presented either independently or separately and it would make sense. In your case, I think having multiple fragments is probably unnecessary because they do not make sense independently of the other (or you would end up with a half-defined event). It seems as if you have one fragment to edit the date and one fragment to edit the name, but conceptually, the two fragments really edit one "object," which is the event. Thus, this should probably be done within one fragment with a ViewFlipper.
Have you seen this?
http://developer.android.com/training/basics/fragments/communicating.html
You need instances of all three fragments in the main activity, and you need to define interfaces for each fragment and implement them in the main activity.
I've seen quite a few questions on SO about Fragments and I still can't seem to figure out if what I want to do is possible, and more so if my design pattern is just flawed and I need to re-work the entire process. Basically, like most questions that have been asked, I have an ActionBar with NavigationTabs (using ActionBarSherlock), then within each Tab there is a FragementActivity and then the FragmentActivities push new Fragments when a row is selected (I'm trying to re-create an iOS Project in Android and it's just a basic Navigation based app with some tabs that can drill down into specific information). When I click the back button on the phone the previous Fragment is loaded but the Fragment re-creates itself (so the WebServices are called again for each view) and this isn't needed since the information won't change in a previous view when going backwards. So basically what I want to figure out is how do I setup my Fragments so that when I push the back button on the phone, the previous Fragment is just pulled up with the previous items already created. Below is my current code :
//This is from my FragmentActivity Class that contains the ActionBar and Tab Selection Control
#Override
public void onTabSelected(ActionBar.Tab tab, FragmentTransaction ft) {
// TODO Auto-generated method stub
int selectedTab = tab.getPosition();
if (selectedTab == 0) {
SalesMainScreen salesScreen = new SalesMainScreen();
ft.replace(R.id.content, salesScreen);
}
else if (selectedTab == 1) {
ClientMainScreen clientScreen = new ClientMainScreen();
ft.replace(R.id.content, clientScreen);
}.....
//This is within the ClientMainScreen Fragment Class, which handles moving to the Detail Fragment
row.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
//Do something if Row is clicked
try{
String selectedClientName = clientObject.getString("ClientName");
String selectedClientID = clientObject.getString("ClientID");
String selectedValue = clientObject.getString("ClientValue");
transaction = getFragmentManager().beginTransaction();
ClientDetailScreen detailScreen = new ClientDetailScreen();
detailScreen.clientID = selectedClientID;
detailScreen.clientName = selectedClientName;
detailScreen.clientValue = selectedValue;
int currentID = ((ViewGroup)getView().getParent()).getId();
transaction.replace(currentID,detailScreen);
transaction.addToBackStack(null);
transaction.commit();
}
catch (Exception e) {
e.printStackTrace();
}
}
});....
//And then this is the Client Detail Fragment, with the method being called to Call the Web Service and create thew (since what is displayed on this screen is dependent on what is found in the Web Service
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup group, Bundle saved) {
return inflater.inflate(R.layout.clientdetailscreen, group, false);
}
#Override
public void onActivityCreated (Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
//Setup Preferences File Link
this.preferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
//initialize the table object
mainTable = (TableLayout)getActivity().findViewById(R.id.mainTable);
//setup the detail table
setupRelatedClientSection();
}
The Client Detail Screen can then drill down one more time, using the same method as the Client Main Screen but when I go back from that new screen to the Detail Screen the seuptRelatedClientSection() method is called again and so the entire Fragment is rebuilt when really I just want to pull up a saved version of that screen. Is this possible with my current setup, or did I approach this the wrong way?
Try using fragementTransaction.add instead of replace
I believe that you are looking for show() and hide().
I think you can still add them to the backstack.
transaction.hide(currentFragment);
transaction.show(detailScreen);
transaction.addToBackStack(null);
transaction.commit();
I didnt have my code to look at but i believe this is how it would go... Try it out unless someone else has a better way.
I have not tried the backstack with show() hide() but i believe that it takes the changes that are made before the transactions commit and will undo them if the back button is pressed. Please get back to me on this cause i am interested to know.
You also have to make sure that the detail fragment is created before you call this. Since it is based on the click of someitem then you should probably create the details fragment every time you click to make sure the correct details fragment is created.
I'm posting this answer for people who may refer this question in future.
Following code will demonstrate how to open FragmentB from FragmentA and going back to FragmentA from FragmentB (without refreshing FragmentA) by pressing back button.
public class FragmentA extends Fragment{
...
void openFragmentB(){
FragmentManager fragmentManager =
getActivity().getSupportFragmentManager();
FragmentB fragmentB = FragmentB.newInstance();
if (fragmentB.isAdded()) {
return;
} else {
fragmentManager.
beginTransaction().
add(R.id.mainContainer,fragmentB).
addToBackStack(FragmentB.TAG).
commit();
}
}
}
public class FragmentB extends Fragment{
public static final String TAG =
FragmentB.class.getSimpleName();
...
public static FragmentB newInstance(){
FragmentB fragmentB = new FragmentB();
return fragmentB;
}
#Override
public void onResume() {
super.onResume();
// add this piece of code in onResume method
this.getView().setFocusableInTouchMode(true);
this.getView().requestFocus();
}
}
In your MainActivity override onBackPressed()
class MainActivity extends Activity{
...
#Override
public void onBackPressed() {
if (getFragmentManager().getBackStackEntryCount() > 0) {
getFragmentManager().popBackStack();
} else {
super.onBackPressed();
}
}
}
You're right, there has been a number of previous questions / documentation on the topic ;)
The documentation on Fragments, specifically the section about Transactions and Saving State, will guide you to the answer.
http://developer.android.com/guide/components/fragments.html#Transactions
http://developer.android.com/guide/components/activities.html#SavingActivityState
Android - Fragment onActivityResult avoid reloading
Fragments can have support for onSaveInstanceState but not onRestoreInstanceState, so if you want to save a reference to the table views, save them to the Bundle and you can access the saved view in your onActivityCreated method. You could also use the Fragments back stack.
This guide/tutorial has very detailed instructions/examples on the back stack and retaining fragment state.
Good luck