I am trying to implement saving and restoring state, but I am running into problems when replacing the main Fragment with a PreferenceFragment and then hitting the back button. My main Fragment consists of a ViewPager with a FragmentPagerAdapter with 3 Fragments to swipe through. None of the Fragment.onCreateView() callbacks for my 3 Fragments are invoked after hitting the back button. I have tried all the solutions I have come over here on SO, but I have not been able to resolve the issue.
Another possibly important thing to note is that the data for my 3 ViewPager Fragments are stored in separate classes which are instanced and accessible through the Activity. The 3 Fragments all contain RecyclerViews for listing a fair amount of data. This is done for cleanliness, but also so that these data persist in the Activity. This is probably not an issue, since it works fine when starting the app, but also since the main issue is that my Fragments are not recreated.
Unexpected behavior:
On application creation, everything works fine, but when I replace my main Fragment (containing a ViewPager with a FragmentPagerAdapter) with another one and then pressing the back button, the Fragments in the ViewPager are not recreated. The onCreateView() of my main Fragment is called.
Questions:
What am I missing? Is there some other callbacks that should be created? Where and how should I recreate the Fragments?
What have I tried:
Changed the FragmentAdapter in use, but I should really use getSupportFragmentManager() as was the solution here.
EDIT: Use of erroneous FragmentManager actually was the source of my troubles. See my answer below.
Add
#Override public int getItemPosition(Object object) {return POSITION_NONE;} to the FragmentPagerAdapter, as suggested here.
Change from FragmentPagerAdapter to FragmentStatePagerAdapter which should not matter here, as far as I understand. The internals of FragmentStatePagerAdapter actually keeps the Fragments, but they are not rendered anew.
Add the main Fragment back using a FragmentTransaction when its onCreateView() is called and in several of the other callbacks of that Fragment, see MMMainFragment below.
Various other things.
My MainFragment class with corresponding XML layout
public class MMMainFragment extends Fragment
{
private MMViewPager mMMViewPager = null;
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
// Note that this method IS called when the back button is pressed.
// I have tried setting the content back to this instance in several places.
View view = inflater.inflate(R.layout.mm_main_fragment, container, false);
// Setup ViewPager.
mMMViewPager = (MMViewPager) view.findViewById(R.id.mm_pager);
TabLayout tabLayout = (TabLayout) view.findViewById(R.id.mm_tablayout);
tabLayout.setupWithViewPager(mMMViewPager);
return view;
}
public MMViewPager getMMViewPager()
{
return mMMViewPager;
}
}
mm_main_fragment.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<android.support.design.widget.TabLayout
android:id="#+id/mm_tablayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabMode="scrollable" />
<com.mycompany.myapp.gui.mmpager.MMViewPager
android:id="#+id/mm_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1" />
</LinearLayout>
Then, in Activity.onCreate() i simply do:
// Insert Main Fragment.
FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.beginTransaction()
.replace(R.id.mm_content_frame, new MMMainFragment())
.commit();
where mm_content_frame is a FrameLayout where I replace and remove Fragments. Then, when a button to view the preferences are pushed, I run the following snippet below in the Activity. I add this Fragment to the backstack to be able to use the back button.
public void showSettingsFragment()
{
getSupportActionBar().setTitle(getString(R.string.mm_drawer_settings));
getSupportFragmentManager().beginTransaction()
.replace(R.id.mm_content_frame, new MMPreferencesFragment(), FragmentConstants.SETTINGS_FRAGMENT_TAG)
.addToBackStack(null)
.commit();
}
The MMViewPager clas:
public class MMViewPager extends ViewPager
{
private MMActivity mMMActivity;
private MMPagerAdapter mMMPagerAdapter;
public MMViewPager(Context context, AttributeSet attrs)
{
super(context, attrs);
mMMActivity = (MMActivity) context;
mMMPagerAdapter = new MMPagerAdapter(mMMActivity.getSupportFragmentManager(), mMMActivity);
this.setOffscreenPageLimit(PagerConstants.OFFSCREEN_PAGE_LIMIT);
this.setAdapter(mMMMPagerAdapter);
this.setCurrentItem(PagerConstants.PAGE_FILTER_RECIPES);
}
}
The MMPagerAdapter class, currently a FragmentStatePagerAdapter:
public class MMPagerAdapter extends FragmentStatePagerAdapter
{
private MMActivity mMMActivity;
public MMPagerAdapter(FragmentManager fragmentManager, MMActivity mmActivity)
{
super(fragmentManager);
mMMActivity = mmActivity;
}
#Override
public Fragment getItem(int position)
{
switch (position)
{
case PagerConstants.PAGE_FILTER_RECIPES:
return new FilteredRecipesFragment();
case PagerConstants.PAGE_SELECTED_RECIPES:
return new SelectedRecipesFragment();
case PagerConstants.PAGE_SHOPPING_LIST:
return new ShoppingListFragment();
default:
return null;
}
}
#Override
public int getCount()
{
return PagerConstants.NUMBER_OF_PAGES; // 3
}
#Override
public CharSequence getPageTitle(int position)
{
switch (position)
{
case PagerConstants.PAGE_FILTER_RECIPES:
return mMMActivity.getResources().getString(R.string.mm_title_recipe_filter_fragment);
case PagerConstants.PAGE_SELECTED_RECIPES:
return mMMActivity.getResources().getString(R.string.mm_title_selected_recipes_fragment);
case PagerConstants.PAGE_SHOPPING_LIST:
return mMMActivity.getResources().getString(R.string.mm_title_shopping_list_fragment);
default:
return "Tab";
}
}
}
For reference, here is also the main layout of the Activity:
<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/mm_drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- Source: http://developer.android.com/training/implementing-navigation/nav-drawer.html -->
<!-- The main content view -->
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- Source: http://android-developers.blogspot.in/2014/10/appcompat-v21-material-design-for-pre.html -->
<android.support.v7.widget.Toolbar
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/mm_toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
android:layout_alignParentTop="true"
style="#style/MMActionBar"
app:theme="#style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="#style/ThemeOverlay.AppCompat.Light" />
<!-- The FrameLayout where I replace Fragments -->
<FrameLayout
android:id="#+id/mm_content_frame"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="#id/mm_toolbar"/>
</RelativeLayout>
<ListView
android:id="#+id/mm_drawer_listview"
android:layout_width="260dp"
android:layout_height="match_parent"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingStart="16dp"
android:layout_gravity="start"
android:choiceMode="singleChoice"
android:divider="#android:color/transparent"
android:dividerHeight="0dp"
android:background="#color/mm_white" />
</android.support.v4.widget.DrawerLayout>
After struggling with this for a few days I realized that the problem actually was that I passed the wrong FragmentManager to the ViewPager. It is indeed the FragmentManager returned by Fragment.getChildFragmentManager() that should be used. This makes sense, since it is the Fragment itself that stores the state of the child Fragments in the ViewPager. I am not sure why I couldn't make that work before, but now the LifeCycle of the app works fine with the following setup in my main Fragment's onCreate() method:
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
View view = inflater.inflate(R.layout.mm_main_fragment, container, false);
mMMActivity = (MMActivity) container.getContext();
mMMActivity.getSupportActionBar().setTitle(getString(R.string.mm_toolbar_title_main_fragment));
// Setup ViewPager.
mViewPager = (ViewPager) view.findViewById(R.id.mm_pager);
mAdapter = new MMPagerAdapter(getChildFragmentManager(), mMMActivity); // <-- This is the key
mViewPager.setAdapter(mAdapter);
mViewPager.setOffscreenPageLimit(PagerConstants.OFFSCREEN_PAGE_LIMIT);
mViewPager.setCurrentItem(PagerConstants.PAGE_FILTER_RECIPES);
// Setup TabLayout.
TabLayout tabLayout = (TabLayout) view.findViewById(R.id.mm_tablayout);
tabLayout.setupWithViewPager(mViewPager);
return view;
}
This was also the solution of another question. As a side note, I use this solution to get the Fragments in my ViewPager and it works just fine with the LifeCycle.
Test Application:
In the research of this issue I created a test application for this. The ones interested can clone it from here. It has ugly colors.
As far as, I understand you have solved your problem. I have faced a quite similar issue in the past. Maybe this help others trying something similar.
The thing that helped me in order to retain my fragment state was to replace the
#Override
public Fragment getItem(int position)
{
switch (position)
{
case PagerConstants.PAGE_FILTER_RECIPES:
return new FilteredRecipesFragment();
case PagerConstants.PAGE_SELECTED_RECIPES:
return new SelectedRecipesFragment();
case PagerConstants.PAGE_SHOPPING_LIST:
return new ShoppingListFragment();
default:
return null;
}
}
method of my adapter.
I change the Adapter so as to keep a HashMap of Fragment. This way the fragments are created once and then retrieved from the HashMap. Depending on your structure you can use an ArrayList instead of the HashMap.
This is done in an efficient and "smart" way of implementing getItemPosition(Object object) to return POSITION_NONE; or the proper position of the item.
I don't really remember if I had the getChildFragmentManager in the adapter or not.
I guess that I could dig into my filesystem if you need further information :)
I've studied all the fragment getArgument null pointer questions already answered and I can't seem to find a solution that works for me. My fragment is functioning properly (buttons doing proper function) except that it does not get any arguments passed in. I have set up my constructor and my calls to it various ways, and cannot avoid the NullPoint. I think it must be something around the lifecycle of the fragment, and the fragment appearing onscreen somehow not being the initiated one, but I can't get to the bottom of it. Any help will be greatly appreciated!
Fragment code:
public class BottomBar extends Fragment {
ImageButton goHome, goPending, goActive, goHelp, goChallenge;
BottomListener activityCallback;
int chalType;
String title;
String text;
//constructor log.d prints out that it has been reached
public static BottomBar init(int chalType, String ttl, String txt){
BottomBar bot = new BottomBar();
Bundle args = new Bundle();
args.putInt("chalType", chalType);
args.putString("title", ttl);
args.putString("text", txt);
bot.setArguments(args);
Log.d("Bottom Bar Init", "called");
return bot;
}
#Override
public void onAttach(Activity activity){
super.onAttach(activity);
try{
activityCallback = (BottomListener) activity;
} catch (ClassCastException e){
Log.d("bottom bar onAttach","class cast");
}
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.bottombarfrag, container, false);
goHome = (ImageButton) view.findViewById(R.id.goHome);
goPending = (ImageButton) view.findViewById(R.id.goPending);
goChallenge = (ImageButton) view.findViewById(R.id.challenge);
goActive = (ImageButton) view.findViewById(R.id.goActive);
goHelp = (ImageButton) view.findViewById(R.id.getInfo);
Bundle argsin = getArguments();
//never enters this loop... argsin always null
if(argsin!=null){
Log.d("bottombar oncreate", "argsin found");
chalType = argsin.getInt("chalType", 0);
title = argsin.getString("title");
text = argsin.getString("text");
}
//.... button activities, functioning properly ....
return view;
}
and main activity code that calls it:
public class MainActivity extends FragmentActivity {
Button btn, paydemo;
TextView tv1, tv2;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String ttl = "Trial1 Home";
String txt = "string1text";
BottomBar bot = BottomBar.init(0, ttl, txt);
getSupportFragmentManager().beginTransaction().add(android.R.id.content, bot).commit();
setContentView(R.layout.activity_main);
btn = (Button) findViewById(R.id.clickbtn);
paydemo = (Button) findViewById(R.id.PayPalDemo);
tv1 = (TextView) findViewById(R.id.textView1);
tv2 = (TextView) findViewById(R.id.textView3);
tv1.setText("try1");
tv2.setText("");
// ...other button actions
}
Follow up:
I've found that I can get the fragment that I want, however it appears as a second instance on the fragment (located on top of display, not where desired), and the original instance is there, but not functioning properly (still with no args). I think that when I initialize the fragment in my activity I am not overwriting the existing fragment that is added by the xml. Maybe I need to initialize the Support Fragment Manager and link it to the proper instance to replace it? Thoughts?
xml where originally intended fragment is located (this is the fragment I want, but not the fragment that is being communicated with).
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<TextView
android:id="#+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Second Layout"
android:textAppearance="?android:attr/textAppearanceLarge" />
<Button
android:id="#+id/retBut"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Return" />
<fragment
android:id="#+id/bottombar"
android:name="com.example.trial1.BottomBar"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:layout_centerHorizontal="true"
android:layout_alignParentBottom="true"
android:layout_marginTop="0dp"
/>
</LinearLayout>
This one is tricky. Take a look at your activity: you are setting fragment and after that calling setContentView. setContentView removes all views from android.R.id.content, i think this breaks fragment's lifecycle somehow. Moreover it looks like a concept mistake: setting fragment and wiping it out after that. I suggest making a fragment container inside R.layout.activity_main, set activity content and after that put you fragment in this container.
So I came up with a functioning answer. I'm not convinced it's the proper way to do it, but it is working.
I created an inherited class for all the classes that reference this fragment, and created a set of public variables to represents the arguments passed to the fragment. I then had each class write the public variables in the super class instead of initiating a new fragment. The fragment then referenced the same public variables to pull information from.
Again, don't think it is the proper way to do it - the argument passing exists to be efficient - but it is working for me now. If anyone has a better idea for how I could accomplish this I'm all ears.
Thanks for all the help
I'm looking for a way to display a DialogFragment in single-pane mode without creating a new activity.
I originally set up a DialogFragment as a popup dialog in my Android app, with the intent to eventually pursue the master-detail pattern for larger devices.
However, now that I'm looking to finally implement the master-detail setup I'm running into all sorts of UI complications. Basically, I'd like to have something like a 'contextual action mode' update to the action bar. That requires some planning in two-pane mode, but it doesn't work at all with a popup dialog (unless I'm missing some way to show the action bar and the popup dialog).
I'd rather not create a new activity to house the detail DialogFragment on non-tablet/large devices, since there is a lot of DB-related code in the existing activity. However, I have trouble just doing FragmentTransaction.replace because the main view is based on a modified FragmentTabsPager from the compatibility lib v4 demo. I don't have a fragment to replace, unless I wrap the entire pager in a fragment - and I'm worried that nesting fragments is a hack that will complicate, rather simplify things in the long run. Am I wrong?
I'm also using ActionBarCompat, which complicates things as there are some UI options that aren't ported. I'd consider going API 11+ if it meant finding a clean solution to this.
BTW I'm starting to look at Commonsware's master-detail library, but it's a bit of code to grok and ingest, and I think it would require a few possibly big changes to make my code compatible.
Any suggestions or comments? I think I'm too close to this one to see how to simplify it...
(1) You should definitely not have to create an entire new Activity whose sole purpose is to house a new DialogFragment. If you are going to create a new activity at all, you might as well just give it a dialog-theme and display that as your dialog instead... that would eliminate the need to show a DialogFragment entirely.
(2) Your question is a bit too general for me to give a confident answer... you mention "contextual action mode", "dialog fragment", "fragment tabs pager", "nested fragments", etc. and I'm not sure how it all fits together or what specifically you are trying to achieve. What I do know is that no matter the configuration, the host activity should always be the one in charge of performing fragment transactions (such as showing a DialogFragment) as this will significantly reduce code complexity (especially when the number of fragments displayed on the screen varies depending on the screen size). Do your fragments communicate with the activity via activity callback methods (as described here and here)? Where in your code do you show the DialogFragment: in your activity or in one of the master/detail fragments?
Try this:
This is the XML of the layout of your Activity.
What you need to do is enclose all your activity content in a layout (like i have done here in id = all_activity_content_id).
Now put two more views in the activity:- A RelativeLayout for your dialog and a View for making the background translucent.
Note: Make sure the root layout of your activity is a RelativeLayout
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="in.curium.testandroid.MainActivity" >
<!-- all your activity's existing code goes here -->
<RelativeLayout
android:id="#+id/all_activity_content_id"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="#android:color/holo_red_light" >
<ToggleButton
android:id="#+id/toggle_dialog_box_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:padding="10dp"
android:textOff="#string/show_dialog"
android:textOn="#string/hide_dialog" />
</RelativeLayout>
<!-- translucent black background behind the dialog -->
<View
android:id="#+id/black_layer_id"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:alpha="0.6"
android:background="#android:color/black"
android:visibility="gone" />
<!-- your dialog layout -->
<RelativeLayout
android:id="#+id/dialog_id"
android:layout_width="200dp"
android:layout_height="150dp"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_marginRight="10dp"
android:background="#00BFFF"
android:visibility="gone" >
<TextView
android:id="#+id/dialog_title_id"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="#android:color/black"
android:text="#string/dialog_title"
android:textColor="#android:color/white" />
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_below="#id/dialog_title_id"
android:text="#string/dialog_description" />
</RelativeLayout>
In your activity class add two methods to show and hide the dialog layout.
Override the back button to dismiss the dialog when visible otherwise call super.
public class MainActivity extends Activity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ToggleButton toggleButton = (ToggleButton) findViewById(R.id.toggle_dialog_box_id);
toggleButton.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View toggleB) {
boolean isOn = ((ToggleButton) toggleB).isChecked();
if (isOn) {
showDialog();
} else {
hideDialog();
}
}
});
}
#Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
#Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
if (id == R.id.action_settings) {
return true;
}
return super.onOptionsItemSelected(item);
}
private void showDialog() {
View opaqueBackground = findViewById(R.id.black_layer_id);
RelativeLayout dialog = (RelativeLayout) findViewById(R.id.dialog_id);
opaqueBackground.setVisibility(View.VISIBLE);
dialog.setVisibility(View.VISIBLE);
}
private void hideDialog() {
View opaqueBackground = findViewById(R.id.black_layer_id);
RelativeLayout dialog = (RelativeLayout) findViewById(R.id.dialog_id);
opaqueBackground.setVisibility(View.GONE);
dialog.setVisibility(View.GONE);
}
#Override
public void onBackPressed() {
ToggleButton toggleB = (ToggleButton) findViewById(R.id.toggle_dialog_box_id);
boolean isOn = toggleB.isChecked();
if (isOn) {
toggleB.setChecked(false);
hideDialog();
} else {
super.onBackPressed();
}
}}
Now any code that you want to put in the dialog will have access to the database adapter that you have in the activity.
Positioning the dialog in the center of your detail fragment will be a bit tricky.
Hope this helps.
Source Code: https://github.com/testv200/DialogInsideAcitvity
It's not possible Because you are using Activity
If you extends FragmentActivity than it's only possible because if you want to show DialogFragment than it's required FragmentManager reference it's not possible in normal Activity. It's provide only inside FragmentActivity or above API level 13
This is my DialogFragment class
public class DetailedFragment extends DialogFragment {
private static final String ARG_SHOW_AS_DIALOG = "DetailedFragment.ARG_SHOW_AS_DIALOG";
public static DetailedFragment newInstance(boolean showAsDialog) {
DetailedFragment fragment = new DetailedFragment();
Bundle args = new Bundle();
args.putBoolean(ARG_SHOW_AS_DIALOG, showAsDialog);
fragment.setArguments(args);
return fragment;
}
public static DetailedFragment newInstance() {
return newInstance(true);
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle args = getArguments();
if (args != null) {
setShowsDialog(args.getBoolean(ARG_SHOW_AS_DIALOG, true));
}
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.detailed_fragment, container, false);
}
}
This is my ActionBarActivity Class
This is the code for showing Dialog inside ActionBarActivity class.I think Please check your import statements
public class DetailedActivity extends ActionBarActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.detailed_activity);
if (savedInstanceState == null) {
DetailedFragment fragment = DetailedFragment.newInstance(false);
getSupportFragmentManager().beginTransaction().add(R.id.root_layout_details, fragment, "Some_tag").commit();
}
}
}
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>