7

How to maintain fragment's state when it is shown within FragmentTabHost?

Thanks to this tutorial, I'm able to implement FragmentTabHost in my application.

My intention is to create an app whose main activity contains some tabs (which sticks on the top throughout the app). Clicking each tab opens a new fragment below the tabs.

Issue is when I click on a tab do something, then go to anther tab which opens a new fragment, then comes back to first tab - my changes here are not maintained.

Flow:

enter image description here

I really need to implement this logic. If my approach is wrong, please suggest alternative.

Thanks

Code:

Main Activity

public class FagTabHostMain extends FragmentActivity {
    FragmentTabHost mTabHost;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_fag_tab_host_main);

        mTabHost = (FragmentTabHost) findViewById(android.R.id.tabhost);
        mTabHost.setup(this, getSupportFragmentManager(), R.id.realtabcontent);

        mTabHost.addTab(mTabHost.newTabSpec("audio").setIndicator("Audio"),
                AudioContainerFragmentClass.class, null);
        mTabHost.addTab(mTabHost.newTabSpec("video").setIndicator("Video"),
                VideoContainerFragmentClass.class, null);

    }

    @Override
    public void onBackPressed() {
        boolean isPopFragment = false;
        String currentTabTag = mTabHost.getCurrentTabTag();
        if (currentTabTag.equals("audio")) {
            isPopFragment = ((AudioContainerFragmentClass) getSupportFragmentManager()
                    .findFragmentByTag("audio")).popFragment();
        } else if (currentTabTag.equals("video")) {
            isPopFragment = ((VideoContainerFragmentClass) getSupportFragmentManager()
                    .findFragmentByTag("video")).popFragment();
        }
        // Finish when no more fragments to show in back stack
        finish();
    }
}

Main activity layout

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <android.support.v4.app.FragmentTabHost
        android:id="@android:id/tabhost"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >

        <!-- Not Using this one right now -->
        <FrameLayout
            android:id="@android:id/tabcontent"
            android:layout_width="0dip"
            android:layout_height="0dip"
            android:layout_weight="0" />

    </android.support.v4.app.FragmentTabHost>

    <FrameLayout
        android:id="@+id/realtabcontent"
        android:layout_width="match_parent"
        android:layout_height="0dip"
        android:layout_weight="1" />

</LinearLayout>

AudioContainerFragmentClass

public class AudioContainerFragmentClass extends Fragment implements
        OnClickListener {

    final String TAG = "AudioContainerFragmentClass";
    private Boolean mIsViewInitiated = false;
    private boolean addToBackStack = true;
    private Button bNextFragment;
    private LinearLayout linearLayout;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        try {
            Log.e("AudioContainerFragmentClass", "onCreateView called");
            linearLayout = (LinearLayout) inflater.inflate(
                    R.layout.audio_fragment_container, container, false);
        } catch (Exception e) {
            printException(e.toString());
        }
        return linearLayout;
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        try {
            super.onActivityCreated(savedInstanceState);
            Log.e("AudioContainerFragmentClass", "onActivityCreated called");
            if (!mIsViewInitiated) {
                mIsViewInitiated = true;
                initView();
            }
        } catch (Exception e) {
            printException(e.toString());
        }
    }

    private void initView() {
        try {
            Log.e("AudioContainerFragmentClass", "initView called");
            bNextFragment = (Button) linearLayout
                    .findViewById(R.id.bNextFragment);
            bNextFragment.setOnClickListener(this);
            replaceFragment(new AudioFragment(), false);
        } catch (Exception e) {
            printException(e.toString());
        }
    }

    private void replaceFragment(AudioFragment audioFragment, boolean b) {
        try {
            FragmentTransaction ft = getChildFragmentManager()
                    .beginTransaction();
            if (addToBackStack) {
                ft.addToBackStack(null);
            }
            ft.replace(R.id.audio_sub_fragment, audioFragment);
            ft.commit();
            getChildFragmentManager().executePendingTransactions();
        } catch (Exception e) {
            printException(e.toString());
        }
    }

    // Called from FagTabHostMain Activity
    public boolean popFragment() {
        boolean isPop = false;
        try {
            Log.e("AudioContainerFragmentClass", "popFragment called");
            if (getChildFragmentManager().getBackStackEntryCount() > 0) {
                isPop = true;
                getChildFragmentManager().popBackStack();
            }
        } catch (Exception e) {
            printException(e.toString());
        }
        return isPop;
    }

    @Override
    public void onClick(View arg0) {
        TextView tv  = (TextView)getActivity().findViewById(R.id.tvaudioTitle);
        tv.setText("Text changed");
    }

    private void printException(String string) {
        Log.e("__ERRORR__", string);
    }
}

AudioFragment

public class AudioFragment extends Fragment {

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.audio_sub_fragment, container,
                false);
        return view;
    }
}
reiley
  • 3,759
  • 12
  • 58
  • 114

4 Answers4

19

Had the same thing in my app. You will need to copy the FragmentTabHost to your project, point your code to use the new custom FragmentTabHost and then change the code of doTabChanged to following implementation:

    private FragmentTransaction doTabChanged(String tabId, FragmentTransaction ft) {
    TabInfo newTab = null;
    for (int i=0; i<mTabs.size(); i++) {
        TabInfo tab = mTabs.get(i);
        if (tab.tag.equals(tabId)) {
            newTab = tab;
        }
    }
    if (newTab == null) {
        throw new IllegalStateException("No tab known for tag " + tabId);
    }
    if (mLastTab != newTab) {
        if (ft == null) {
            ft = mFragmentManager.beginTransaction();
        }
        if (mLastTab != null) {
            if (mLastTab.fragment != null) {
                ft.hide(mLastTab.fragment);
            }
        }
        if (newTab != null) {
            if (newTab.fragment == null) {
                newTab.fragment = Fragment.instantiate(mContext,
                        newTab.clss.getName(), newTab.args);
                ft.add(mContainerId, newTab.fragment, newTab.tag);
                findViewById(mContainerId).setContentDescription("DEBUG. add fragment to this container");
            } else {
                if (newTab.fragment.isHidden()){
                    ft.show(newTab.fragment);
                }
                else{
                    ft.attach(newTab.fragment); 
                }
            }
        }

        mPreviousTab = mLastTab;
        mLastTab = newTab;
    }
    return ft;
}

The change that was made is that instead of deattach/attach the fragment, we are doing hide/show

Sean
  • 5,176
  • 2
  • 34
  • 50
  • Nice logic, let me try it. What is the value `mTabs` is storing in line three. Also, have you posted the full snippet somewhere for reference. Also how to call `doTabChanged` – reiley Oct 20 '13 at 09:58
  • @raul8, the full code snippet is practically the original `FragmentTabHost` with only the changes I've posted above. In order to start using this code, simply 1. copy the entire code to a new class in your project (code can be found here: http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.2.2_r1/android/support/v4/app/FragmentTabHost.java) 2. Replace the method code with the one I've posted. 3. In your `FagTabHostMain` Class, use the new class for your member ` FragmentTabHost mTabHost;` – Sean Oct 20 '13 at 10:14
  • Thanks @Sean for your answer. But I guess you have not put all your code here. Because on screen rotation I have get some headache, because you hide the previous tab using the mLastTab with that code `if (mLastTab != null) { if (mLastTab.fragment != null) { ft.hide(mLastTab.fragment); } }`. You should showed that previous hidden tab in onAttachedToWindow with a code like `if(tab.fragment != null && tab.fragment.isHidden()){ ft.show(tab.fragment); }` just before to detach the fragment. – hermannovich Aug 11 '15 at 21:38
  • FragmentTabHost.java is colliding with my library android-support-v4-appcompat. I'm getting the error like this :- Unable to execute dex: Multiple dex files define Landroid/support/v4/app/FragmentTabHost$DummyTabFactory. I'm using eclipse for this project. – Likith Ts Aug 25 '15 at 11:58
  • i think your code is not changed right,the fourth line code count from end is error.i think should change "attach"method to "show"method – perry Aug 19 '16 at 08:51
2

I believe your fragment is being re-instantiated each time you switch tab, which means that your field variables are reset.

You probably could use the saveInstance bundle to manage the state of your fragment but I find it more useful and simpler to use SharedPreferences. This also has the benefit of keeping the saved state even if your application is restarted.

To read and write variables to SharedPreferences I use this small helper class:

public class PreferencesData {

public static void saveString(Context context, String key, String value) {
    SharedPreferences sharedPrefs = PreferenceManager
            .getDefaultSharedPreferences(context);
    sharedPrefs.edit().putString(key, value).commit();
}

public static void saveInt(Context context, String key, int value) {
    SharedPreferences sharedPrefs = PreferenceManager
            .getDefaultSharedPreferences(context);
    sharedPrefs.edit().putInt(key, value).commit();
}


public static void saveBoolean(Context context, String key, boolean value) {
    SharedPreferences sharedPrefs = PreferenceManager
            .getDefaultSharedPreferences(context);
    sharedPrefs.edit().putBoolean(key, value).commit();
}

public static int getInt(Context context, String key, int defaultValue) {
    SharedPreferences sharedPrefs = PreferenceManager
            .getDefaultSharedPreferences(context);
    return sharedPrefs.getInt(key, defaultValue);
}

public static String getString(Context context, String key, String defaultValue) {
    SharedPreferences sharedPrefs = PreferenceManager
            .getDefaultSharedPreferences(context);
    return sharedPrefs.getString(key, defaultValue);
}

public static boolean getBoolean(Context context, String key, boolean defaultValue) {
    SharedPreferences sharedPrefs = PreferenceManager
            .getDefaultSharedPreferences(context);
    return sharedPrefs.getBoolean(key, defaultValue);
}
}

Now, as an example, to save your mIsViewInitiated variable, then in onPause:

@Override
protected void onPause() {
    PreferencesData.saveBoolean(this, "isViewInitiated", mIsViewInitiated);
    super.onPause();
}

And to retrieve it again:

@Override
public void onActivityCreated(Bundle savedInstanceState) {
    try {
        super.onActivityCreated(savedInstanceState);
        Log.e("AudioContainerFragmentClass", "onActivityCreated called");
        // will now be true if onPause have been called
        mIsViewInitiated = PreferencesData.getBoolean(this, "isViewInitiated", false);
        if (!mIsViewInitiated) {
            mIsViewInitiated = true;
            initView();
        }
    } catch (Exception e) {
        printException(e.toString());
    }
}

Since this example variable tells whether some UI has been loaded, then you might want to set it to false when the activity is destroyed.

@Override
protected void onDestroy() {
    PreferencesData.saveBoolean(this, "isViewInitiated", false);
    super.onDestroy();
}

This answer is just a single option and shows my personal preference, whereas other options might suit your situation better. I would suggest taking a look at http://developer.android.com/guide/topics/data/data-storage.html

cYrixmorten
  • 7,110
  • 3
  • 25
  • 33
  • Your answer was very good, but I was trying to avoid shared prefs as the data to retain was in large qualtities. Even the answer provided by Sean is needs further handling, but it does provide me a start. Thank You – reiley Oct 21 '13 at 06:55
  • 1
    No problem. As I do not see any answers in this direction, I would suggest perhaps looking at the API demo examples of retaining a GoogleMap fragment. I would think that you should be able to combine setRetainInstance(true) and keeping a reference to the fragments in order to reinstantiate the correct (retained) fragment on tab changes. Just do give you yet another option to look at. – cYrixmorten Oct 21 '13 at 09:32
0

Modify your Activity to override onSaveInstanceState and your onCreate method to restore from a "savedInstanceState".

public static final String TAB_STATE = "TAB_STATE";

@Override
protected void onSaveInstanceState(Bundle outState) {
    outstate.putParcelable(TAB_STATE, mTabHost.onSaveInstanceState());
    super.onSaveInstanceState(outState);
}

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_fag_tab_host_main);

    mTabHost = (FragmentTabHost) findViewById(android.R.id.tabhost);
    mTabHost.setup(this, getSupportFragmentManager(), R.id.realtabcontent);
    if(savedInstanceState==null || savedInstanceState.getParcelable(TAB_STATE)==null){
    mTabHost.addTab(mTabHost.newTabSpec("audio").setIndicator("Audio"),
            AudioContainerFragmentClass.class, null);
    mTabHost.addTab(mTabHost.newTabSpec("video").setIndicator("Video"),
            VideoContainerFragmentClass.class, null);
    } else{
        mTabHost.onRestoreInstanceState(savedInstanceState.getParcelable(TAB_STATE));
    }

}
ejohansson
  • 2,832
  • 1
  • 23
  • 30
  • Thanks for reply but, not able to call `mTabHost.onSaveInstanceState()` inside `onSaveInstanceState()` method. Is this working for you? – reiley Oct 18 '13 at 05:16
0

As stated above you can save and then restore your data via the Bundle, Shared Preferences, or a SQLite db. You may also want to call setRetainInstance(true) on your Fragment. This will stop your fragments from being re-created repeatedly.

SoundsDangerous
  • 708
  • 6
  • 13
  • I tried this method but was not able to retain the state. May be i was doing it wrong. Could you please explain with example? – reiley Oct 18 '13 at 05:12