EDIT:
After some more tinkering around, I found out the reason for my previous problem (see below. TL;DR: I'm trying to pass a Bundle from an activity to its fragment by replacing the fragment) is that when replacing fragments like this:
AddEditActivityFragment fragment = new AddEditActivityFragment();
Bundle arguments = getIntent().getExtras();
fragment.setArguments(arguments);
getSupportFragmentManager().beginTransaction()
.replace(R.id.add_edit_fragment, fragment)
.commit();
the replaced fragment does not get destroyed (as it should be according to a bunch of sources), which I verified by overriding and adding logging to onPause, onStop, onDestroyView, onDestroy, etc. None of them are called.
Calling popBackStackImmediate also does nothing.
What can I do?
Previous question title:
"Setting FloatingActionButton icon dynamically with setImageDrawable doesn't have any effect."
I have a FAB inside a fragment (called AddEditFragment), that serves to save user input to database.
To differentiate between editing rows from the DB and creating new ones, I had its icon set either a "send" icon or a "save" icon.
By default, the button is set to "send":
<android.support.design.widget.FloatingActionButton
android:id="#+id/add_edit_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="#dimen/fab_margin"
app:layout_anchor="#id/add_edit_toolbar"
app:layout_anchorGravity="bottom|right|end"
app:srcCompat="#drawable/ic_send_white_24dp"/>
I used to set the icon to "save" by implementing an interface in my fragment, which serves to pass data from the containing activity to the fragment, using this method:
#Override
public void receiveData(Serializable data) {
Log.d(TAG, "receiveData: called");
mFoodItem = (FoodItem) data;
if (mFoodItem != null) {
mEditMode = true;
fab.setImageDrawable(ContextCompat.getDrawable(getContext(), R.drawable.ic_save_white_24dp));
utilDisplayFoodItem();
}
}
The call the setImageDrawable worked fine and changed the icon from "send" to "save" properly.
Here is when I run into trouble.
I am trying to remove my AddEditFragment class' dependency on my AddEditActivity class, by removing said interface implementation and passing the data required by the fragment via Bundle:
#Override
protected void onCreate(Bundle savedInstanceState) {
Log.d(TAG, "onCreate: starts");
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_add_edit);
AddEditActivityFragment fragment = new AddEditActivityFragment();
Bundle arguments = getIntent().getExtras();
boolean editMode = arguments != null;
fragment.setArguments(arguments);
getSupportFragmentManager().beginTransaction()
.replace(R.id.add_edit_fragment, fragment)
.commit();
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_close_white_24dp);
getSupportActionBar().setTitle((editMode) ? "Edit Item:" : "Create Item:");
getSupportActionBar().setElevation(0);
Log.d(TAG, "onCreate: ends");
}
In this context, I removed receiveData method from the fragment, and placed this line:
fab.setImageDrawable(ContextCompat.getDrawable(getContext(), R.drawable.ic_save_white_24dp));
in various likely places in my fragment class (the likeliest being inside its onCreateView method).
It doesn't seem to have any effect. My cases are:
Just adding said setImageDrawable call - sets icon to "save" in both add/edit modes, but then I have no "send" icon.
Setting either case dynamically (setImageDrawable call is inside if block) - icon is set to "send" in both add/edit modes.
Removing default "send" icon from XML, then setting either case dynamically - icon is set to "send" in both add/edit modes.
Removing default icon from XML, then setting only to "save" dynamically (no if block) - sets icon to "save" in both add/edit modes, but then I have no "send" icon.
It seems the setImageDrawable call, which worked perfectly in my receiveData interface method, doesn't have any effect (at least when an icon is already set, or when inside if block).
I'm at a loss and would appreciate any help!
In reply to #ColdFire:
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Log.d(TAG, "onCreateView: starts");
View view = inflater.inflate(R.layout.fragment_add_edit, container, false);
ButterKnife.bind(this, view);
Bundle arguments = getArguments();
if (arguments != null) {
mFoodItem = (FoodItem) arguments.getSerializable(FoodItem.class.getSimpleName());
if (mFoodItem != null) {
mEditMode = true;
// Tried calling setImageDrawable here
}
}
// Tried calling setImageDrawable here
if (!mEditMode) {
// If adding an item, initialize it for right now's date and time
Calendar now = Calendar.getInstance();
mFoodItem.setDate(now.get(Calendar.YEAR), now.get(Calendar.MONTH), now.get(Calendar.DAY_OF_MONTH));
mFoodItem.setTime(now.getTimeInMillis() / Constants.MILLISECONDS);
}
utilDisplayFoodItem();
utilSetOnClickListeners();
setHasOptionsMenu(true);
// Tried calling setImageDrawable here
Log.d(TAG, "onCreateView: ends");
return view;
}
I should mention that everything else that depends on the Bundle data works correctly.
You might want to clear back stack by using the following method
private void clearBackStack() {
FragmentManager manager = getSupportFragmentManager();
if(manager.getBackStackEntryCount() > 0) {
FragmentManager.BackStackEntry first = manager.getBackStackEntryAt(0);
manager.popBackStackImmediate(first.getId(), FragmentManager.POP_BACK_STACK_INCLUSIVE);
}
}
You might want to check this post, Thanks
This will only destroy the current fragment to be replaced. If you want to destroy all you might want to read this stack question
Related
I'm using BottomNavigationView in android to make a application just like Instagram. I'm using fragments with the navigationTabs. App have 5 tabs initialy I've set the middle tab as active tab and loads it once the app start. when i click on any other tab a network call is made and data is loaded. Now when i press on back button or click on the last tab again(which was loaded on startup) the fragment is recreated and the network call is made to load the same data. I want to show the previous fragments with same data without recreating.
I've tried using
transaction.add(container,fragment);
but to no avail.
my code on tab click
if (item.getItemId() == R.id.nav_OverView && _current != R.id.nav_OverView) {
Overview _overView = new Overview();
_fragmentTransaction.hide(_currentFragment);
_fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_CLOSE);
_fragmentTransaction.add(R.id.content_base_drawer, _overView);
_fragmentTransaction.commit();
_current = R.id.nav_OverView;
viewIsAtHome = true;
}
I know using remove and add is same as using replace.
Any help is appreciated.
Before creating the view you can check if the view is already created or not, the below code helps fragment recreating problem.
View view;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState)
{
if (view == null)
{
view = inflater.inflate(R.layout.frag_layout, container, false);
init(view);
}
return view;
}
Make _overview into a class field (instead of a local variable), and change your listener code to this:
Also, use replace instead of hide+add, this will prevent Fragment already added errors.
Replace an existing fragment that was added to a container. This is
essentially the same as calling remove(Fragment) for all currently
added fragments that were added with the same containerViewId and then
add(int, Fragment, String) with the same arguments given here.
See example:
if (item.getItemId() == R.id.nav_OverView && _current != R.id.nav_OverView) {
if (_overView == null) {
_overView = new Overview();
}
_fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_CLOSE);
_fragmentTransaction.replace(R.id.content_base_drawer, _overView);
_fragmentTransaction.commit();
_current = R.id.nav_OverView;
viewIsAtHome = true;
}
I have an Activity with a FrameLayout and need to show different fragments based on user input.
The code I use for showing a fragment is this:
private void showFragment(Fragment fragment, Bundle args, boolean addToBackStack) {
if (args != null) {
fragment.setArguments(args);
}
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
fragmentTransaction.setCustomAnimations(R.anim.activity_open_translate, R.anim.activity_close_scale);
fragmentTransaction.replace(R.id.main_frame, fragment);
if (addToBackStack) {
fragmentTransaction.addToBackStack(fragment.getClass().getName());
}
fragmentTransaction.commit();
}
This is called as :
if (contactPickFragment == null) {
contactPickFragment = new ContactPickFragment();
}
showFragment(contactPickFragment, args, true);
All this works fine. Now if the user goes into one fragment presses back and returns back to the same fragment, all my views inside stay the same. For example, I have an EditText inside the fragment and the user edits something inside. If the user comes back to the fragment, the same text persists. I do not want this to happen. How do I reset everything in the view?
I have added code within the Fragment's onCreateView() to clear the text, and from debugging I see that this is being called, but the text never gets cleared. What am I doing wrong here?
If you don't want the data from the previous instance to appear, simply create a new instance of ContactPickFragment each time you show it.
Clearing data in onCreateView() has no effect because view state is restored AFTER onCreateView(). Your Fragment has no view before onCreateView() and so Android cannot possibly apply the previous state any earlier. Values set on the views during onCreateView() will be overwritten by their previous values.
As a general answer, there is no way to "refresh" the view of a Fragment, other than replacing the fragment with another instance of itself (possibly initialized with the parameters that you want to refresh/update).
You can reuse your fragments and refresh the state of your views. You just can't do it from onCreateView as #antonyt correctly points out.
Instead, override onViewStateRestored and set up the state of your views the way you'd like from there.
Something like:
#Override
public void onViewStateRestored(Bundle savedInstanceState) {
super.onViewStateRestored(savedInstanceState);
View view = getView();
// Code to call view.findViewById to grab the views you want
// and set them to a specific state goes here
}
There are advantages to reusing fragments. Not the least of which is that if you have a memory leak with your fragment (which is easier than you may think to accomplish,) you will exacerbate the problem by creating myriads of them.
Problems with app:
When orientation changes the app is experiencing these problems:
Both FragmentA and FragmentC now occupy the FrameLayout container.
What works: Everything works as I want it to...prior to rotating the screen.
Activity description in brief:
EditActivity Purpose: edit collection and item fields.
Fragments this activity programmatically creates:
FragmentA - fragment for editing collection fields
FragmentB - ListFragment of items in collection
FragmentC - fragment for editing item fields.
Initial layout: FragmentA sits atop FragmentB, each in their own FrameLayouts.
When user clicks FragmentB's listview item: replace FragmentA with FragmentC to allow user to edit that item's fields. Now FragmentC sits atop FragmentB.
This seems like a very simple notion: the top portion of the activity is for editing either properties of the collection as a whole or a single item from the collection. I don't feel I have done anything wondrous with the layout so I'm a fair bit perplexed that a simple rotation of the phone (emulator) causes these problems that I am having such a dastardly time trying to fix.
Why the Android Fragment Guide example doesn't work for me: their example is much like what I am doing but their detail fragment is either being opened in a new activity or in its own Frame within the current activity, they don't do any swapping of fragments so I cannot glean how they would use the onSaveIstanceState to preserve the fragments that are visible and then use that information in onCreate to recreate the UI that was there prior to orientation change.
EDIT: took out one problem by caving and putting the listfragment in the XML, this solved the perpetual spinning "loading..." problem.
Solved. Oh, the rabbit holes I traveled... At any rate, if you run into problems like this a couple of things to consider:
ultimately I didn't have to write any code in onSaveInstanceState(Bundle outState).
Ultimately I didn't have to make any considerations about handling the backstack in onSaveInstanceState or deal with it the activity's onCreate.
When first "adding" fragments programmatically to the FrameLayout, use replace instead of `add' - this was likely one of the roots of my troubles.
in onCreate check if savedInstanceState's bundle is null, if(savedInstanceState == null), and if it is then I know that the activity hasn't been torn down previously by a configuration change, so here I build fragments that should be displayed right at activity start up. Other fragments that are programmatically brought to life elsewhere (ie, later than the activity's onCreate()), they don't belong in the if, they belong in the else:
else onSaveInstanceState != null and I know there's only one reason this thing's not null, because the system made a bundle named outState in onSaveInstanceState(Bundle outState) and hucked it at the activity's onCreate method where I can now get my grubbies on it. So it is here that I know a couple of things:
for sure the fragments I created in the activity's onCreate are still a part of the activity (I didn't detach or destroy them), but, I cannot make that same claim for the fragments brought to life via a user's actions, those fragments may or may not be currently (at the time of orientation aka configuration change) attached to the activity.
This is a good place for an if-this-thing-is-attached clause. One of things I initially messed up on was I failed to give ALL of my programmatically added fragments a tag; give all programmatically added fragments tags. I can then find out if the savedInstanceState bundle contains that key with savedInstanceState.containsKey(MY_FRAG_TAG) and with getFragmentManager().findFragmentByTag(MY_FRAG_TAG)
So here's the activity's onCreate (simplified):
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_edit);
// ...omitted code...
if(savedInstanceState == null){
// create fragment for collection edit buttons
editCollection = FragmentA.newInstance(someVariable);
// programmatically add fragment to ViewGroup
getFragmentManager().beginTransaction().replace(R.id.edit_topFrame, editCollection, EDIT_COLLECTIONS_TAG).commit();
}
// else there be stuff inside the savedInstanceState bundle
else{
// fragments that will always be in the savedInstanceState bundle
editCollectionFragment = (FragmentA)getFragmentManager().findFragmentByTag(EDIT_COLLECTIONS_TAG);
// fragments that may not be in the bundle
if(savedInstanceState.containsKey(EDIT_ITEM_TAG)){
editItemFragment = (FragmentC)getFragmentManager().getFragment(savedInstanceState, EDIT_ITEM_TAG);
}
}
// This fragment is NOT programmatically added, ie, it is statically found in an XML file.
// Hence, the system will take care of preserving this fragment on configuration changes.
listFrag = (ListViewFragment)getFragmentManager().findFragmentById(R.id.ListFragment);
// create adapter
adapter = new EditCursorAdapter(this, null);
// set list fragment adapter
listFrag.setListAdapter(adapter);
// prepare the loader
getLoaderManager().initLoader(LOADER_ID, null, this);
}
And the Activity's listener for the list fragment, where FragmentC is swapped for FragmentA:
// listfragment listener
#Override
public void listFragListener(Cursor cursor) {
// checking backstack size
Log.d(TAG, SCOPE +"backstack size: "+getFragmentManager().getBackStackEntryCount());
// With each listview click there should be only one item in the backstack.
getFragmentManager().popBackStack();
// create new fragment
editItemFragment = FragmentC.newInstance(cursor);
// programmatically add new fragment
FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.replace(R.id.edit_topFrame, editItemFragment, EDIT_ITEM_TAG);
ft.addToBackStack("pop all of these"); // was testing different ways of popping
ft.commit();
// interesting: this reports the same value as the first log in this method.
// ...clearly addToBackStack(null).commit() doesn't populate the backstack immediately?
Log.d(TAG, SCOPE +"backstack size: "+getFragmentManager().getBackStackEntryCount());
}
And onSaveInstanceState is naked as a jay bird:
#Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
}
Summary: I have the activity functioning exactly as I want it to.
Now, if I had a bunch of added fragments then I might handle them in a more programmatic fashion rather than by hard coding the if(savedInstanceState.contains(*hard coded key*). This I tested a little bit but cannot attest to its efficacy, however for someone out there this might spark an idea of what you can do:
Make a private Set of added fragments:
// Collection of Frag Tags
private Set<String> AddedFragmentTagsSet = new HashSet<String>();
In onAttachFragment do something like:
#Override
public void onAttachFragment(Fragment fragment) {
super.onAttachFragment(fragment);
// logging which fragments get attached and when
Log.d(TAG, SCOPE +"attached fragment: " +fragment.toString());
// NOTE: XML frags have not frigg'n tags
// add attached fragment's tag to set of tags for attached fragments
AddedFragmentTagsSet.add(fragment.getTag());
// if a fragment has become detached remove its tag from the set
for(String tag : AddedFragmentTagsSet){
if(getFragmentManager().findFragmentByTag(tag).isDetached()){
AddedFragmentTagsSet.remove(tag);
}
Log.d(TAG, SCOPE +"contents of AddedFragmentTagsSet: " +tag);
}
}
Then in the activity's onCreate and within savedInstanceState clauses:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_edit);
// ...omitted code...
if(savedInstanceState == null){
// create fragment for collection edit buttons
editCollection = FragmentA.newInstance(someVariable);
// programmatically add fragment to ViewGroup
getFragmentManager().beginTransaction().replace(R.id.edit_topFrame, editCollection, EDIT_COLLECTIONS_TAG).commit();
}
// else there be stuff inside the savedInstanceState bundle
else{
// fragments that will always be in the savedInstanceState bundle
editCollectionFragment = (FragmentA)getFragmentManager().findFragmentByTag(EDIT_COLLECTIONS_TAG);
//////////// find entries that are common to AddedFragmentTagsSet & savedInstanceState's set of keys ///////////
Set<String> commonKeys = savedInstanceState.keySet();
commonKeys.retainAll(AddedFragmentTagsSet);
for(String key : commonKeys){
editItemFragment = FragmentC)getFragmentManager().getFragment(savedInstanceState, key);
}
}
}
...but that is untested and presented merely to spark ideas; in trying to figure out what was wrong with my activity's handling of configuration changes I did stumble and fumble in this direction and think it might bear fruit for the right person; though ultimately, obviously, I found a simpler way to fix my issues this time around.
Info: I have a 2 pane layout (2 child Fragments) inside a ParentFragment, which, of course, is inside a FragmentActivity. I have setRetainInstance(true) on the ParentFragment. On orientation change, the left child fragment doesn't get destroyed (onCreate() doesn't get called), which is normal (because of the parent retaining its instance).
Problem: On orientation change, the right fragment gets destroyed (onCreate() gets called). Why the hell is the right fragment destroyed and the left one isn't ?
EDIT: If I remove setRetainInstance(true), then the left fragment's onCreate() gets called twice (lol wtf) and the right fragment's onCreate() gets called once. So this isn't good either...
Code below for the ParentFragment:
#Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
this.setRetainInstance(true);
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState)
{
View view = inflater.inflate(R.layout.fragment_schedule, container, false);
setHasOptionsMenu(true);
if (getChildFragmentManager().findFragmentById(R.id.fragment_schedule_framelayout_left) == null ||
!getChildFragmentManager().findFragmentById(R.id.fragment_schedule_framelayout_left).isInLayout())
{
if (mPresentationsListFragment == null)
mPresentationsListFragment = PresentationsListFragment.newInstance(PresentationsListFragment.TYPE_SCHEDULE, mScheduleDate);
getChildFragmentManager().beginTransaction()
.replace(R.id.fragment_schedule_framelayout_left, mPresentationsListFragment)
.commit();
}
mPresentationsListFragment.setOnPresentationClickListener(this);
return view;
}
#Override
public void onPresentationClick(int id)
{
if (Application.isDeviceTablet(getActivity()))
{
if (getChildFragmentManager().findFragmentById(R.id.fragment_schedule_framelayout_right) == null)
{
if (mPresentationDetailFragment == null)
mPresentationDetailFragment = PresentationDetailFragment.newInstance(id);
else
mPresentationDetailFragment.loadPresentation(id);
getChildFragmentManager().beginTransaction()
.replace(R.id.fragment_schedule_framelayout_right, mPresentationDetailFragment)
.commit();
}
else
mPresentationDetailFragment.loadPresentation(id);
}
else
{
Intent presentationDetailIntent = new Intent(getActivity(), PresentationDetailActivity.class);
presentationDetailIntent.putExtra(PresentationDetailActivity.KEY_PRESENTATION_ID, id);
startActivity(presentationDetailIntent);
}
}
LE Solution:
Thanks a lot to antonyt , the answer is below. The only changes needed to pe performed reside inside onCreateView() of the parent Fragment.
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState)
{
View view = inflater.inflate(R.layout.fragment_schedule, container, false);
setHasOptionsMenu(true);
if (getChildFragmentManager().findFragmentById(R.id.fragment_presentations_framelayout_left) == null)
{
mPresentationsListFragment = PresentationsListFragment.newInstance();
mPresentationsListFragment.setOnPresentationClickListener(this);
getChildFragmentManager().beginTransaction()
.add(R.id.fragment_presentations_framelayout_left, mPresentationsListFragment)
.commit();
}
return view;
}
From what I understand, if you have setRetainInstance(true) on the parent fragment with the above code, your left fragment should be recreated but your right fragment should not be, when changing orientation. This is backwards to what you wrote above, but I will explain why this is the case anyway. If you have setRetainInstance(false) on the parent fragment, you indeed should see the left fragment being created twice and the right fragment being created once.
Case 1: setRetainInstance(true)
Your parent fragment will not be destroyed on rotation. However, it will still recreate its views each time (onDestroyView and onCreateView will be called, in that order). In onCreateView you have code to add your left fragment under certain conditions. getChildFragmentManager().findFragmentById(R.id.fragment_schedule_framelayout_left) should be non-null, since a fragment was added to that container previously. getChildFragmentManager().findFragmentById(R.id.fragment_schedule_framelayout_left).isInLayout() should be false since only fragments added via XML will cause it to return true. The overall condition is true and so a new instance of your left fragment will be created and it will replace the old one. Your right fragment is only instantiated during a click event and so no special behavior happens.
Summary: Parent fragment remains, new left fragment is created, right fragment remains.
Case 2: setRetainInstance(false)
Your parent fragment is destroyed, and so are the left and right fragments. All three fragments are recreated automatically by Android. Your parent fragment will then get a chance to create its view, and it will create a new instance of the left fragment as per the explanation above. The just-created left fragment will be replaced by this new instance. You will observe that a left fragment will be destroyed and another left fragment will be created. No special behavior happens for the right fragment.
Summary: New parent fragment is created, two new left fragments are created, new right fragment is created.
If you are sure that in the setRetainInstance(true) case, your right fragment is being destroyed and not your left one, please post a sample project to github/etc. that demonstrates this.
Update: Why the right fragment gets removed if you use FragmentTransaction.replace() on the left fragment
Because of the inner conditional, your code will try to replace your left fragment with itself on the same container.
Here is the code snippet from the Android 4.1 source code that handles a replace:
...
case OP_REPLACE: {
Fragment f = op.fragment;
if (mManager.mAdded != null) {
for (int i=0; i<mManager.mAdded.size(); i++) {
Fragment old = mManager.mAdded.get(i);
if (FragmentManagerImpl.DEBUG) Log.v(TAG,
"OP_REPLACE: adding=" + f + " old=" + old);
if (f == null || old.mContainerId == f.mContainerId) {
if (old == f) {
op.fragment = f = null;
} else {
if (op.removed == null) {
op.removed = new ArrayList<Fragment>();
}
op.removed.add(old);
old.mNextAnim = op.exitAnim;
if (mAddToBackStack) {
old.mBackStackNesting += 1;
if (FragmentManagerImpl.DEBUG) Log.v(TAG, "Bump nesting of "
+ old + " to " + old.mBackStackNesting);
}
mManager.removeFragment(old, mTransition, mTransitionStyle);
}
}
}
}
if (f != null) {
f.mNextAnim = op.enterAnim;
mManager.addFragment(f, false);
}
} break;
...
If you try to replace the same fragment with itself, there is some code to try and ignore this operation:
if (old == f) {
op.fragment = f = null;
}
Since f is null, and we are still continuing to iterate through our fragments, this seems to have the side effect of removing every subsequent fragment from the FragmentManager. I don't think this is intentional, but at the very least explains why your right fragment is getting destroyed. Not using replace / not replacing the same fragment with itself can fix your issues.
Interestingly, this was a recent change and did not exist in previous versions of Android.
https://github.com/android/platform_frameworks_support/commit/5506618c80a292ac275d8b0c1046b446c7f58836
Bug report: https://code.google.com/p/android/issues/detail?id=43265
I have a problem reloading an activity with tabs and fragments when I change the orientation of my device.
Here's the situation:
I have an activity which has 3 tabs in the action bar. Each tab loads a different fragment in a FrameLayout in main view. Everything works fine if I don't change the orientation of the device. But when I do that Android tries to initialize the currently selected fragment twice which produce the following error:
E/AndroidRuntime(2022): Caused by: android.view.InflateException: Binary XML file line #39: Error inflating class fragment
Here's the sequence of steps that produce the error:
I load the activity, select tab nr 2. and change the orientation of the device.
Android destroys the activity and the instance of the fragment loaded by tab nr 2 (from now on, 'Fragment 2'). Then it proceeds to create new instances of the activity and the fragment.
Inside Activity.onCreate() I add the first tab to the action bar. When I do that, this tab gets automatically selected. It may represent a problem in the future, but I don't mind about that now. onTabSelected gets called and a new instance of the first fragment is created and loaded (see code below).
I add all the other tabs without any event being triggered, which is fine.
I call ActionBar.selectTab(myTab) to select Tab nr 2.
onTabUnselected() gets called for the first tab, and then onTabSelected() for the second tab. This sequence replaces the current fragment for an instance of Fragment 2 (see code below).
Next, Fragment.onCreateView() is called on Fragment 2 instance and the fragment layout gets inflated.
Here is the problem. Android Calls onCreate() and then onCreateView() on the fragment instance ONCE AGAIN, which produces the exception when I try to inflate (a second time) the layout.
Obviously the problem is Android is initializing the fragment twice, but I don't know why.
I tried NOT selecting the second tab when I reaload the activity but the second fragment gets initialized anyway and it is not shown (since I didn't select its tab).
I found this question: Android Fragments recreated on orientation change
The user asks basically the same I do, but I don't like the chosen answer (it's only a workaroud). There must be some way to get this working without the android:configChanges trick.
In case it's not clear, what I want to know how whether to prevent the recreation of the fragment or to avoid the double initialization of it. It would be nice to know why is this happening also. :P
Here is the relevant code:
public class MyActivity extends Activity implements ActionBar.TabListener {
private static final String TAG_FRAGMENT_1 = "frag1";
private static final String TAG_FRAGMENT_2 = "frag2";
private static final String TAG_FRAGMENT_3 = "frag3";
Fragment frag1;
Fragment frag2;
Fragment frag3;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// my_layout contains a FragmentLayout inside
setContentView(R.layout.my_layout);
// Get a reference to the fragments created automatically by Android
// when reloading the activity
FragmentManager fm = getFragmentManager();
this.frag1 = fm.findFragmentByTag(MyActivity.TAG_FRAGMENT_1);
this.frag2 = fm.findFragmentByTag(MyActivity.TAG_FRAGMENT_2);
this.frag3 = fm.findFragmentByTag(MyActivity.TAG_FRAGMENT_3)
ActionBar actionBar = getActionBar();
// snip...
// This triggers onTabSelected for the first tab
actionBar.addTab(actionBar.newTab()
.setText("Tab1").setTabListener(this)
.setTag(MyActivity.TAG_FRAGMENT_1));
actionBar.addTab(actionBar.newTab()
.setText("Tab2").setTabListener(this)
.setTag(MyActivity.TAG_FRAGMENT_2));
actionBar.addTab(actionBar.newTab()
.setText("Tab3").setTabListener(this)
.setTag(MyActivity.TAG_FRAGMENT_3));
Tab t = null;
// here I get a reference to the tab that must be selected
// snip...
// This triggers onTabUnselected/onTabSelected
ab.selectTab(t);
}
#Override
protected void onDestroy() {
// Not sure if this is necessary
this.frag1 = null;
this.frag2 = null;
this.frag3 = null;
super.onDestroy();
}
#Override
public void onTabSelected(Tab tab, FragmentTransaction ft) {
Fragment curFrag = getFragmentInstanceForTag(tab.getTag().toString());
if (curFrag == null) {
curFrag = createFragmentInstanceForTag(tab.getTag().toString());
if(curFrag == null) {
// snip...
return;
}
}
ft.replace(R.id.fragment_container, curFrag, tab.getTag().toString());
}
#Override
public void onTabUnselected(Tab tab, FragmentTransaction ft)
{
Fragment curFrag = getFragmentInstanceForTag(tab.getTag().toString());
if (curFrag == null) {
// snip...
return;
}
ft.remove(curFrag);
}
private Fragment getFragmentInstanceForTag(String tag)
{
// Returns this.frag1, this.frag2 or this.frag3
// depending on which tag was passed as parameter
}
private Fragment createFragmentInstanceForTag(String tag)
{
// Returns a new instance of the fragment requested by tag
// and assigns it to this.frag1, this.frag2 or this.frag3
}
}
The code for the Fragment is irrelevant, it just returns an inflated view on onCreateView() method override.
I got a simple answer for that:
Just add setRetainInstance(true); to the Fragment's onAttach(Activity activity) or onActivityCreated(Bundle savedInstanceState).
These two are call-backs in the Fragment Class.
So basically, what setRetainInstance(true) does is:
It maintains the state of your fragment as it is, when it goes through:
onPause();
onStop();
It maintains the instance of the Fragment no matter what the Activity goes through.
The problem with it could be, if there are too many Fragments, it may put a strain on the System.
Hope it helps.
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
setRetainInstance(true);
}
Open for Correction as always. Regards, Edward Quixote.
It seems that, when the screen is rotated and the app restarted, it is recreating each Fragment by calling the default constructor for the Fragment's class.
I have encountered the same issue and used the following workaround:
in the fragment's onCreateView begining of:
if (mView != null) {
// Log.w(TAG, "Fragment initialized again");
((ViewGroup) mView.getParent()).removeView(mView);
return mView;
}
// normal onCreateView
mView = inflater.inflate(R.layout...)
I think this is a fool proof way to avoid re-inflating of the root view of the fragment:
private WeakReference<View> mRootView;
private LayoutInflater mInflater;
/**
* inflate the fragment layout , or use a previous one if already stored <br/>
* WARNING: do not use in any function other than onCreateView
* */
private View inflateRootView() {
View rootView = mRootView == null ? null : mRootView.get();
if (rootView != null) {
final ViewParent parent = rootView.getParent();
if (parent != null && parent instanceof ViewGroup)
((ViewGroup) parent).removeView(rootView);
return rootView;
}
rootView = mFadingHelper.createView(mInflater);
mRootView = new WeakReference<View>(rootView);
return rootView;
}
#Override
public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) {
mInflater=inflater!=null?inflater:LayoutInflater.from(getActivity());
final View view = inflateRootView();
... //update your data on the views if needed
}
add
android:configChanges="orientation|screenSize"
in the manifest file
To protect activity recreate try to add configChanges in your Activity tag (in manifest), like:
android:configChanges="keyboardHidden|orientation|screenSize"
My code was a little different, but I believe our problem is the same.
In the onTabSelected I didn't use replace, I use add when is the first time creating the fragment and attach if isn't. In the onTabUnselected I use detach.
The problem is that when the view is destroyed, my Fragment was attached to the FragmentManager and never destroyed. To solve that I implemented on the onSaveInstanceBundle to detach the fragment from the FragmentManager.
The code was something like that:
FragmentTransition ft = getSupportFragmentManager().begin();
ft.detach(myFragment);
ft.commit();
In the first try I put that code in the onDestroy, but I get a exception telling me that I couldn't do it after the onSaveInstanceBundle, so I moved the code to the onSaveInstanceBundle and everything worked.
Sorry but the place where I work don't allow me to put the code here on StackOverflow. This is what I remember from the code. Feel free to edit the answer to add the code.
I think you are facing what I faced. I had a thread downloader for json which starts in onCreate() , each time I changed the orientation the thread is called and download is fired. I fixed this using onSaveInstance() and onRestoreInstance() to pass the json response in a list, in combination of checking if the list is not empty, so the extra download is not needed.
I hope this gives you a hint.
I solved this problem by using below code.
private void loadFragment(){
LogUtil.l(TAG,"loadFragment",true);
fm = getSupportFragmentManager();
Fragment hf = fm.findFragmentByTag("HOME");
Fragment sf = fm.findFragmentByTag("SETTING");
if(hf==null) {
homeFragment = getHomeFragment();// new HomeFragment();
settingsFragment = getSettingsFragment();// new Fragment();
fm.beginTransaction().add(R.id.fm_place, settingsFragment, "SETTING").hide(settingsFragment).commit();
fm.beginTransaction().add(R.id.fm_place, homeFragment, "HOME").commit();
activeFragment = homeFragment;
}else{
homeFragment = hf;
settingsFragment = sf;
activeFragment = sf;
}
}
Initiate this method in OnCreate();