I recently refactored a application and replaced a ViewFlipper for a FrameLayout on which I swap between Fragments.
Each time user request one of the views:
public void showLibraryOf(long publisherId) {
library = new DownloadLibraryFragment(id, viewFactory());
FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.replace(R.id.container, library);
ft.commit();
library.setAdapterObserver(this);
}
public void showMyLibraryOf(long publisherId) {
myLibrary = new MyLibraryFragment(id, viewFactory());
FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.replace(R.id.container, myLibrary);
ft.commit();
}
public void showHelp() {
FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.replace(R.id.container, new HelpFragment());
ft.commit();
}
I create a new Fragment and replace the old one. Those being removed from screen get onDestroy called, but the memory consumed by the bitmaps I load on the screen does not get removed, so the application crashes after some swap between the fragments.
I also tried to remove references at onDestroy
#Override
public void onDestroyView() {
destroy();
super.onDestroyView();
adapter.clear();
adapter.clearObservers();
adapter.notifyDataSetChanged();
view.setAdapter(new ArrayAdapter<Journal>(getActivity(), 0));
adapter = null;
view = null;
}
But the memory keeps growing.
Anyone knows any solution? maybe reuse fragments? effectively destroy it? I'm listening.
I forget which stackoverflow question I originally pulled this code from, but one method that seems to work well is to override onAttachFragment of the FragmentActivity, and then store a WeakReference to each fragment passed in. Then, instead of using the replace method of a FragmentTransaction, you recycle all the fragments (as relevant for the case).
Here's an example of additional members and methods on a FragmentActivity that creates a default fragment in onCreate and responds to changes via onNewIntent:
private List<WeakReference<Fragment>> mFragments =
new ArrayList<WeakReference<Fragment>>();
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.add(R.id.fragment_container, MyFragment.newInstance("default"));
ft.commit();
}
#Override
protected void onNewIntent(Intent intent) {
setIntent(intent);
String section = intent.getStringExtra("section");
recycleFragments();
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.add(R.id.fragment_container, MyFragment.newInstance(section));
ft.commit();
}
#Override
public void onAttachFragment(Fragment fragment) {
mFragments.add(new WeakReference<Fragment>(fragment));
}
private void recycleFragments() {
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
for (WeakReference<Fragment> ref : mFragments) {
Fragment fragment = ref.get();
if (fragment != null) {
ft.remove(fragment);
}
}
ft.commit();
}
Now if you monitor the heap, you should notice it's not blowing up in size. This solution mostly comes into play when you have nested fragments containing bitmaps which for some reason don't seem to get recycled properly. I'd love a more elegant solution but this one works.
Related
In the following piece of code, what's the point of using detach/attach fragments instead of just replacing them?
private void showFragment(String tag) {
String oldTag = mSelectedTag;
mSelectedTag = tag;
final FragmentManager fm = getSupportFragmentManager();
final FragmentTransaction ft = fm.beginTransaction();
final Fragment oldFragment = fm.findFragmentByTag(oldTag);
final Fragment fragment = fm.findFragmentByTag(tag);
if (oldFragment != null && !tag.equals(oldTag)) {
ft.detach(oldFragment);
}
if (fragment == null) {
ft.replace(R.id.container, getContentFragment(tag), tag);
} else {
if (fragment.isDetached()) {
ft.attach(fragment);
}
}
ft.commit();
}
Why can't I just write something like this?
private void showFragment(String tag) {
final FragmentManager fm = getSupportFragmentManager();
final FragmentTransaction ft = fm.beginTransaction();
ft.replace(R.id.container, getContentFragment(tag), tag);
ft.addToBackStack(null);
ft.commit();
}
getContentFragment method, just in case:
private Fragment getContentFragment(String tag) {
Fragment fragment = null;
if (Frag1.TAG.equals(tag)) {
fragment = new Frag1();
} else if (Frag2.TAG.equals(tag)) {
fragment = new Frag2();
}
return fragment;
}
Here's the documentation for FragmentTransaction.detach() (emphasis added):
Detach the given fragment from the UI. This is the same state as when it is put on the back stack: the fragment is removed from the UI, however its state is still being actively managed by the fragment manager. When going into this state its view hierarchy is destroyed.
So a detached fragment is still "alive" inside the FragmentManager; its view has been destroyed but all of its logical state is preserved. So when you call attach(), you are getting the same fragment back.
FragmentTransaction.replace(), passing a new fragment, however, will cause you to wind up using two different instances of the same fragment class, rather than re-using a single instance.
Personally, I've never had a need to use detach() and attach(), and have always used replace(). But that doesn't mean that there isn't a place and time where they're going to be useful.
I have a google map inside a fragment. If I go to outside of the app while I am using the fragment, on my return I can't see the map. But if I generally move from one fragment to another, there is no problem. I am using navigation drawer, so I have to use the fragment.
Any solution would be appreciated. Thank you.
Here is the part of that fragment related to onPause(). I think I have done some mistakes with the code, plz kindly point that out to me.
#Override
public void onPause() {
super.onPause();
final FragmentManager fragManager = this.getFragmentManager();
final Fragment fragment = fragManager.findFragmentById(R.id.map1);
if(fragment!=null){
fragManager.beginTransaction().remove(fragment).commit();
}
}
#Override
public void onDestroy() {
super.onDestroy();
final FragmentManager fragManager = this.getFragmentManager();
final Fragment fragment = fragManager.findFragmentById(R.id.map1);
if(fragment!=null){
fragManager.beginTransaction().remove(fragment).commit();
}
}
You can fix that Exception . In your fragment where you are showing map, override onDestroyView().
public void onDestroyView() {
super.onDestroyView();
Fragment fragment = (getFragmentManager().findFragmentById(R.id.map));
FragmentTransaction ft = getActivity().getSupportFragmentManager().beginTransaction();
ft.remove(fragment);
ft.commit();
}
And i think you need to start from API 16.
Hope this helps.
At last I got the solution. Now it's working, the app doesn't crash anymore.
#Override
public void onDestroyView() {
super.onDestroyView();
try{
MapFragment fragment = ((MapFragment) getFragmentManager().findFragmentById(R.id.map));
FragmentTransaction fragTran = getActivity().getFragmentManager().beginTransaction();
ft.remove(fragment);
ft.commit();
}catch(Exception e){
}
}
I show animation when user navigates away from fragment. For that I am using setCustomAndimations of support package.
"popEnter" and "popExit" work fine, but they are lost after activity gets rotated,
i.e. after rotation popping fragment happens without the animation.
Fragment creation in activity:
#Override
protected void onCreate(Bundle savedInstanceState) {
...
if (savedInstanceState == null) { // activity started for the first time, no fragment attached yet
fragment = MyFragment.newInstance(params);
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.setCustomAnimations(0, 0, // enter animations, not important here
// when popping fragment -> these are lost on rotation
R.anim.slide_in_right, R.anim.slide_out_right);
ft.add(R.id.content, fragment, MY_TAG).addToBackStack(null).commit();
}
}
Is there way / workaround to keep animating "popping out" of fragment after rotation ?
I found a temporarily solution for this problem here (answer #3)
fix this by adding:
android:configChanges="orientation|screenSize"
to your FragmentActivity in the manifest.
There are of course problems, when you i.e. inflate different layouts in your onCreateView, depending on the screen size. So thats also not a final answer.
Edit: you can create your own backstack:
public final class MBackStack {
public static Stack<Fragment> fragStack = new Stack<>();
private MBackStack(){}
public static void addFragment(Fragment frag){
fragStack.push(frag);
}
public static Fragment getFragment(){
if (fragStack.isEmpty()) {
return null;
}
fragStack.pop();
Fragment fragment = fragStack.peek();
return fragment;
}
public static int getStackSize(){
return fragStack.size();
}
public static void clearStack(){
while (fragStack.size()!=0){
fragStack.pop();
}
}
}
Now instead of
ft.addToBackStack(null);
You can add:
YOURFRAGMENT yf = new YOURFRAGMENT();
MBackStack.addFragment(yf);
ft = getSupportFragmentManager().beginTransaction();
ft.replace(R.id.content_frame, dts);
ft.commit();
And in your main activity you can override your onbackpressed:
#Override
public void onBackPressed() {
if(MBackStack.getStackSize()>1){
ft = getSupportFragmentManager().beginTransaction();
ft.setCustomAnimations(R.anim.in_left, R.anim.out_right);
ft.replace(R.id.content_frame, MBackStack.getFragment());
ft.commit();
}else{
finish();
overridePendingTransition(R.anim.no_animation, R.anim.slide_bottom_out);
}
}
I have tried it, and it works fine.
I am working on the following tutorial, it has problems : it recreates the fragments after each screen rotation.
I fixed it concerning the TitlesFragment class by adding if(savedInstanceState == null) in QuoteViewerActivity:
mFragmentManager = getFragmentManager();
//ADDED THIS CONDITION
if(savedInstanceState == null){
FragmentTransaction fragmentTransaction = mFragmentManager
.beginTransaction();
fragmentTransaction.add(R.id.title_fragment_container, mTitlesFragment);
fragmentTransaction.commit();
}
it fixed it for Fragment TitlesFragment however for Fragment QuoteFragment it is still recreating it on each screen orientation change because in this tutorial that fragment is created in an onclick event:
#Override
public void onListSelection(int index) {
if (!mDetailsFragment.isAdded()) {
FragmentTransaction fragmentTransaction = mFragmentManager
.beginTransaction();
fragmentTransaction.add(R.id.quote_fragment_container, mDetailsFragment);
fragmentTransaction.addToBackStack(null);
fragmentTransaction.commit();
mFragmentManager.executePendingTransactions();
}
if (mDetailsFragment.getShownIndex() != index) {
mDetailsFragment.showIndex(index);
}
}
note that setRetainInstance(true) is set in both fragment's onCreate().
I tried to add this checking but it didn't fix it:
#Override
public void onListSelection(int index) {
//ADDED THE FOLLOWING TWO LINES
Fragment f = mFragmentManager.findFragmentById(R.id.quote_fragment_container);
if(f == null)
//===============================
if (!mDetailsFragment.isAdded()) {
FragmentTransaction fragmentTransaction = mFragmentManager
.beginTransaction();
fragmentTransaction.add(R.id.quote_fragment_container, mDetailsFragment);
fragmentTransaction.addToBackStack(null);
fragmentTransaction.commit();
mFragmentManager.executePendingTransactions();
}
if (mDetailsFragment.getShownIndex() != index) {
mDetailsFragment.showIndex(index);
}
}
==> It recreates this Fragment each time I rotate the screen and duplicates existing menus (explained in this snapshot):
What am I doing wrong and what is the best practice to fix this? thanks!
This line won't find the fragment you're trying to find:
Fragment f = mFragmentManager.findFragmentById(R.id.quote_fragment_container);
You provided container (layout) id. This method can be used to find fragments that were inflated from XML layout.
If you want to manage fragments from code, use tag. Add a fragment using FragmentTransaction.add(int containerViewId, Fragment fragment, String tag). Providing a tag you can later find that fragment using FragmentManager.findFragmentByTag(String tag). It's a good idea to make tag some kind of static final String constant, making automatic refactoring a breeze.
You may be also interested with method FragmentTransaction.replace(int containerViewId, Fragment fragment, String tag) - it makes fragment replacement easier.
Solution:
In this specific tutorial the solution for this problem was solved by:
using onSaveInstanceState to store QuoteFragment state to its containing activity
Getting/handling the QuoteFragment by checking if it is found in the savedInstanceState
Here is what I added/changed in the code:
.....
private QuoteFragment mDetailsFragment = new QuoteFragment();//REMOVED final attribute
......
//ADDED
#Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if(mFragmentManager.findFragmentByTag(quote_fragment_tag)!=null)
getFragmentManager().putFragment(outState, QuoteFragment.class.getName(), mDetailsFragment);
}
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
TitleArray = getResources().getStringArray(R.array.Titles);
QuoteArray = getResources().getStringArray(R.array.Quotes);
setContentView(R.layout.main);
mFragmentManager = getFragmentManager();
// ADDED
if(savedInstanceState == null){
FragmentTransaction fragmentTransaction = mFragmentManager
.beginTransaction();
fragmentTransaction.add(R.id.title_fragment_container, mTitlesFragment);
fragmentTransaction.commit();
}
else{//ADDED
mDetailsFragment = (QuoteFragment) getFragmentManager()
.getFragment(savedInstanceState, QuoteFragment.class.getName());
if(mDetailsFragment == null){
mDetailsFragment = new QuoteFragment();
mFragmentManager.beginTransaction()
.add(R.id.quote_fragment_container, mDetailsFragment,quote_fragment_tag)
.addToBackStack(null)
.commit();
mFragmentManager.executePendingTransactions();
}
}
}
Note: in my humble opinion for best practices concerning fragments and config changes on runtime check Google's official tutorial.
Another simple solution.Add
menu.clear()
before inflating the menu in onCreateOptionsMenu method inside the fragment
#Override
public void onCreateOptionsMenu(Menu menu,MenuInflater inflater) {
menu.clear();
inflater.inflate(R.menu.menu_main, menu);
}
I have a layout that has an EditText and a Button. I <include> it in my main layout.
I'm having a weird issue with the layout and rotation. It seems to duplicate itself when the device (physical) is rotated, messing up the text and layout.
Here it is on first open, after I add some extra garble:
DSC_0013 is in the EditText on launch of the fragment.
Then, I rotate the phone and add some different garble:
And you can see the issue pretty clearly. At first, I thought it was just the EditText messing up. But if I add enough text to make a new line:
I can see that the button gets messed up too.
I do override onSaveInstanceState, but in it I don't touch the EditText or its value, it's strictly used for something else.
What's happening and how do I fix it?
Fixed it!
Turns out it wasn't the view duplicating itself, or the EditText, or the Button. It was the entire fragment.
In my Activity's onCreate, I add the fragment to an xml layout:
private FileDetails fileDetailsFragment;
public void onCreate(Bundle savedInstanceState) {
...
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager
.beginTransaction();
fileDetailsFragment = new FileDetails(fileData);
fragmentTransaction.add(R.id.DetailsHolder, fileDetailsFragment);
fragmentTransaction.commit();
And onCreate was being called every time I rotated the phone (as it's meant to). So I put in a check to see if the activity is being run for the first time, and it works great.
private FileDetails fileDetailsFragment;
public void onCreate(Bundle savedInstanceState) {
...
if (savedInstanceState == null) {
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager
.beginTransaction();
fileDetailsFragment = new FileDetails(fileData);
fragmentTransaction.add(R.id.DetailsHolder, fileDetailsFragment);
fragmentTransaction.commit();
} else {
fileDetailsFragment = (FileDetails) getSupportFragmentManager().findFragmentById(R.id.DetailsHolder);
}
You can also setRetainedInstance(true) on your fragment, then try to get the Fragment form de FragmentManager.findFragmentById(int) or FragmentManager.findFragmentByTag(String), and if it returns null it meant you had to create a new instance of your Fragment.
private FileDetails fileDetailsFragment;
public void onCreate(Bundle savedInstanceState) {
...
FragmentManager fragmentManager = getSupportFragmentManager();
fileDetailsFragment = (FileDetails) getSupportFragmentManager().findFragmentById(R.id.DetailsHolder);
if (fileDetailsFragment == null) {
fileDetailsFragment = new FileDetails(FileData);
}
FragmentTransaction fragmentTransaction = fragmentManager
.beginTransaction();
fragmentTransaction.add(R.id.DetailsHolder, fileDetailsFragment);
fragmentTransaction.commit();
}
In some cases, the value of savedInstanceState may be null after rotation, so it is better to add another condition:
FragmentManager fragmentManager = getSupportFragmentManager();
if (savedInstanceState == null &&
fragmentManager.getFragments().size() == 0) {
FragmentTransaction fragmentTransaction = fragmentManager
.beginTransaction();
fileDetailsFragment = new FileDetails(fileData);
fragmentTransaction.add(R.id.DetailsHolder, fileDetailsFragment);
fragmentTransaction.commit();
} else {
fileDetailsFragment = (FileDetails)
getSupportFragmentManager().findFragmentById(R.id.DetailsHolder);
}