I am creating a Fragment Activity with tabs. Now whenever i select a tab, corresponding fragment gets recreated and its
onCreateView()
method is called.
But what i want is to reuse the view of the fragment so that whenever a tab is selected system does not call
onCreateView()
of fragment instead it shows the previous view of the fragment if it exists.
Please reply soon.
You may want to show and hide the fragments instead of adding and removing them when selecting a tab.
For example:
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
if (frag.isAdded()) {
transaction.show(R.id.layout, frag);
} else {
transaction.add(R.id.layout, frag);
}
transaction.commit();
setOffScreenPageLimit() method can be used to set how many fragments you want to keep alive even if they are not visible to the user.
Try this, it works for screen rotation and should work for your situation as well:
1) When you add fragment for the first time, create it with name parameter:
getSupportFragmentManager()
.beginTransaction()
.replace(<containerID>, <fragment>, YOUR_FRAGMENT_NAME_CONST)
.commit();
2) When you need to find it:
YourFragmentType fragment;
fragment = (YourFragmentType) getSupportFragmentManager()
.findFragmentByTag(YOUR_FRAGMENT_NAME_CONST);
if (fragment != null){
//TODO set data to the existing fragment
}
else{
//TODO create and initialize your fragment
}
Edit:
You should differentiate Fragment object creation and onCreateView() being called.
It's right that you should avoid unnecessary object creation and so reuse fragments.On the other hand (as for my experience) it's better to adhere Android's onCreateView() politics to guarantee consistent user experience.
If you really want to save CPU time and avoid re-inflating complicated view (and settle all issues yourselves) - you may just check it is null like this:
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
// View
if (mView == null) {
mView = inflater.inflate(R.layout.your_fragment, container, false);
...
}
...
}
Related
The main page of my application has a FrameLayout.
I'm instantiating two fragments when the activity starts, and I'm trying to use a menu button to swap between the fragment.
scanHistoryFrag = new HistoryFragment();
scanFrag = new ScanFragment();
I never replace these objects - I use the same ones throughout the lifecycle of the application. However, when I swap them in my FrameLayout...
private void ChangeFragment(Android.Support.V4.App.Fragment fragment)
{
Android.Support.V4.App.FragmentTransaction ft = SupportFragmentManager.BeginTransaction();
ft.Replace(Resource.Id.fragmentContainer, fragment);
ft.Commit();
}
OnCreate and OnCreateView are called on the Fragment again... which means any adjustments I made post creation on that fragment are overwritten with initial values again. I can't seem to find any explanation for why this is happening or how I might avoid it.
The ChangeFragment method is being called by OnOptionsItemSelected, as I'm using a menu button to toggle them.
I never replace these objects - I use the same ones throughout the lifecycle of the application.
Initialization of a subclass of Fragment just create a instance of this class object, the constructor of this class will be called, but it will not go through the lifecycle of Fragment unless this Fragment is added, for more information, you can refer to Fragments. To understand it easier, I personal think the instance saves the data state of this Fragment class, but the events of lifecycle handle the view state of this Fragment.
which means any adjustments I made post creation on that fragment are overwritten with initial values again.
Yes, you're right. To avoid overwritting with initial values again, we can cache the fragment's view in OnCreateView for example like this:
private View rootView;
public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
// Use this to return your custom view for this Fragment
// return inflater.Inflate(Resource.Layout.YourFragment, container, false);
if (rootView == null)
{
//first time creating this fragment view
rootView = inflater.Inflate(Resource.Layout.fragmentlayout1, container, false);
//Initialization
//TODO:
}
else
{
//not first time creating this fragment view
ViewGroup parent = (ViewGroup)rootView.Parent;
if (parent != null)
{
parent.RemoveView(rootView);
}
}
return rootView;
}
Here's the problem
I have a Fragment class DisplayFragment and I already have one show in the content frame, then I do
DisplayFragment a = DisplayFragment.newInstance();
getSupportFragmentManager().beginTransaction()
.replace(R.id.contentFrame, DisplayFragment)
.commit();
Then I want to get the view of fragment a using View v = a.getView();, but it return a null view.
Can anyone tell me why? Cause I have to change some view setting of the new Fragment.
onCreateView() in DisplayFragment
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_display, container, false);
//Some TextView setup
Button button = (Button) view.fineViewById(R.id.button);
return view; }
You need to use delay or put your View v = a.getView(); in loop which will watch first that your fragment is successfully attached, created and added in your activity or not. For this you can check with this isAdded() and isInLayout() if both return true then only call getView()
Now why this, as you add/replace fragment with commit with will fragment will class will be execute first it'll be start Fragment life cycle that is onAttach(), onCreate(), onCreateView() and so on. Now you will getting null from getView() just because your view is not created still. Fragment Life Cycle. If you doubt regarding this let me know.
I assume that you call View v = a.getView(); right after commit?
Because getView method only return not null value after onCreateView returned.
In your case, after called commit(), it take time to complete all the lifecyle callback of DisplayFragment a (from onCreate -> onCreatedView,..).
So that, right after commit(), the getView method still return null.
Declare DisplayFragment a; as Class Reference. Because after commit you want to just access View v = a.getView(); it unavailable due to fragment Life Cycle. You can get after execute block of code.
Make sure you have to mentioned OnCreateView() Method on class DisplayFragment
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
if (container == null) {
// We have different layouts, and in one of them this
// fragment's containing frame doesn't exist. The fragment
// may still be created from its saved state, but there is
// no reason to try to create its view hierarchy because it
// won't be displayed. Note this is not needed -- we could
// just run the code below, where we would create and return
// the view hierarchy; it would just never be used.
return null;
}
Log.i("Right", "onCreateView()");
return inflater.inflate(R.layout.right, container, false);
}
I had same problem as like you.
As the transaction of fragment did not takes place immediately. Thats why sometime the findViewById() does not work properly. But we can execute the pending transaction by using executePendingTransactions() method. I have used following code in my project to execute the pedingTransactions.
Fragment f = (Fragment)(FragmentClass.class).newInstance();
FragmentManager fm = getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
ft.replace(R.id.your_fragment_container_name,f).commit();
fm.executePendingTransactions(); //Notice the FragmentManager Class object
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.
I have an app with hierarchy like this:
FragmentTabHost (Main Activity)
- Fragment (tab 1 content - splitter view)
- Fragment (lhs, list)
- Framment (rhs, content view)
- Fragment (tab 2 content)
- Fragment (tab 2 content)
All fragment views are being inflated from resources.
When the app starts everything appears and looks fine. When I switch from the first tab to another tab and back again I get inflate exceptions trying to recreate tab 1's views.
Digging a little deeper, this is what's happening:
On the first load, inflating the splitter view causes its two child fragments to be added to the fragment manager.
On switching away from the first tab, it's view is destroyed but it's child fragments are left in the fragment manager
On switching back to the first tab, the view is re-inflated and since the old child fragments are still in the fragment manager an exception is thrown when the new child fragments are instantiated (by inflation)
I've worked around this by removing the child fragments from the fragment manager (I'm using Mono) and now I can switch tabs without the exception.
public override void OnDestroyView()
{
var ft = FragmentManager.BeginTransaction();
ft.Remove(FragmentManager.FindFragmentById(Resource.Id.ListFragment));
ft.Remove(FragmentManager.FindFragmentById(Resource.Id.ContentFragment));
ft.Commit();
base.OnDestroyView();
}
So I have a few questions:
Is the above the correct way to do this?
If not, how should I be doing it?
Either way, how does saving instance state tie into all of this so that I don't lose view state when switching tabs?
I'm not sure how to do this in Mono, but to add child fragments to another fragment, you can't use the FragmentManager of the Activity. Instead, you have to use the ChildFragmentManager of the hosting Fragment:
http://developer.android.com/reference/android/app/Fragment.html#getChildFragmentManager()
http://developer.android.com/reference/android/support/v4/app/Fragment.html#getChildFragmentManager()
The main FragmentManager of the Activity handles your tabs.
The ChildFragmentManager of tab1 handles the split views.
OK, I finally figured this out:
As suggested above, first I changed the fragment creation to be done programatically and had them added to the child fragment manager, like so:
public override View OnCreateView(LayoutInflater inflater, ViewGroup viewGroup, Bundle savedInstance)
{
var view = inflater.Inflate(Resource.Layout.MyView, viewGroup, false);
// Add fragments to the child fragment manager
// DONT DO THIS, SEE BELOW
var tx = ChildFragmentManager.BeginTransaction();
tx.Add(Resource.Id.lhs_fragment_frame, new LhsFragment());
tx.Add(Resource.Id.rhs_fragment_frame, new RhsFragment());
tx.Commit();
return view;
}
As expected, each time I switch tabs, an extra instance of Lhs/RhsFragment would be created, but I noticed that the old Lhs/RhsFragment's OnCreateView would also get called. So after each tab switch, there would be one more call to OnCreateView. Switch tabs 10 times = 11 calls to OnCreateView. This is obviously wrong.
Looking at the source code for FragmentTabHost, I can see that it simply detaches and re-attaches the tab's content fragment when switching tabs. It seems the parent Fragment's ChildFragmentManager is keeping the child fragments around and automatically recreating their views when the parent fragment is re-attached.
So, I moved the creation of fragments to OnCreate, and only if we're not loading from saved state:
public override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
if (savedInstanceState == null)
{
var tx = ChildFragmentManager.BeginTransaction();
tx.Add(Resource.Id.lhs_fragment_frame, new LhsFragment());
tx.Add(Resource.Id.rhs_fragment_frame, new RhsFragment());
tx.Commit();
}
}
public override View OnCreateView(LayoutInflater inflater, ViewGroup viewGroup, Bundle savedInstance)
{
// Don't instatiate child fragments here
return inflater.Inflate(Resource.Layout.MyView, viewGroup, false);
}
This fixed the creation of the additional views and switching tab's basically worked now.
The next question was saving and restoring view state. In the child fragments I need to save and restore the currently selected item. Originally I had something like this (this is the child fragment's OnCreateView)
public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstance)
{
var view = inflater.Inflate(Resource.Layout.CentresList, container, false);
// ... other code ommitted ...
// DONT DO THIS, SEE BELOW
if (savedInstance != null)
{
// Restore selection
_selection = savedInstance.GetString(KEY_SELECTION);
}
else
{
// Select first item
_selection =_items[0];
}
return view;
}
The problem with this is that the tab host doesn't call OnSaveInstanceState when switching tabs. Rather the child fragment is kept alive and it's _selection variable can be just left alone.
So I moved the code to manage selection to OnCreate:
public override void OnCreate(Bundle savedInstance)
{
base.OnCreate(savedInstance);
if (savedInstance != null)
{
// Restore Selection
_selection = savedInstance.GetString(BK_SELECTION);
}
else
{
// Select first item
_selection = _items[0];
}
}
public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstance)
{
// Don't restore/init _selection here
return inflater.Inflate(Resource.Layout.CentresList, container, false);
}
Now it all seems to be working perfectly, both when switching tabs and changing orientation.
My Android application has an ActionBar that changes which Fragment occupies a certain FrameLayout. I am trying to use onSaveInstanceState to save the state of a Fragment when the tab is changed, so that it can be recovered in onCreateView.
The problem is, onSaveInstanceState is never called. The Fragment's onDestroyView and onCreateView methods are called, but the Bundle supplied to onCreateView remains null.
Can someone please explain to me when onSaveInstanceState is actually called, how I can make sure it gets called when switching tabs, or the best practice for saving and restoring the state of a Fragment when it is detached and re-attached?
Fragment:
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.event_log, container, false);
// Retrieve saved state
if (savedInstanceState != null){
System.out.println("log retrieved");
} else {
System.out.println("log null");
}
return view;
}
#Override
public void onSaveInstanceState(Bundle outState) {
System.out.println("log saved");
super.onSaveInstanceState(outState);
// more code
}
Activity:
/**
* Detach the current Fragment, because another one is being attached.
*/
#Override
public void onTabUnselected(Tab tab, FragmentTransaction ft) {
if (tab.getText().equals(getString(R.string.tab_events))){
if (frEventLog != null) {
ft.detach(frEventLog);
}
}
Fragment#onSaveInstanceState is only called when the Activity hosting the Fragment is destroyed AND there is a chance that you can come back to the same activity AND the fragment is still added to the FragmentManager. The most common case would be screen rotation.
I think your Fragment will also need to do setRetainInstance(true) in onCreate for example. Not exactly sure about that point though.
You should also see this method being called when you press the home button for example. That will destroy the activity but you can go back to it by using the task list for example.
If you just detach() the fragment all you need to do to get it back is to ask the FragmentManager for it.
There are two examples you should have a look at:
ActionBar FragmentTabs and TabHost FragmentTabs
The TabHost example uses
ft.add(containerId, fragment, tag);
// later
fragment = mActivity.getSupportFragmentManager().findFragmentByTag(tag);
to find the instances of previously added Fragments, works until you remove() a Fragment
Regarding onCreateView / onDestroyView: That is called once a fragment gets detached because the next time you attach it needs to create a new View. Note that Fragment#onDetached() is not called when you detach() the fragment because it is still attached to the Activity. It is only detached from the view-hierarchy.
There is another nice example on how to retain fragment state / how to use fragments to retain state in Android Training - Caching Bitmaps.
That example is missing a critical line though:
public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {
RetainFragment fragment = (RetainFragment) fm.findFragmentByTag(TAG);
if (fragment == null) {
fragment = new RetainFragment();
fm.beginTransaction().add(fragment, TAG).commit(); // << add this
}
return fragment;
}