I have this activity where I run an asynctask that gathers a series of data. This data can be represented in several ways, for the sake of simplicity let's say there's two ways. I want the user to be able to switch between these two representations (listview and graphical view).
So I created a FragmentActivity with my ListFragment and my GraphicFragment.
How do you switch between these two so that user can go back and forth? I tried using replace but then the previous fragment gets destroyed which is not of my interest since its reference has the data passed by my Activity. (i.e. I don't want to recreate the fragment each times the user switches between views).
I also tried hiding and showing but I can't get it to work, one of the fragments is not showing.
Also, when I rotate I guess the fragments get destroyed because I get a nullpointerexception when trying to access them.
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_layout);
pBar = (ProgressBar) findViewById(R.id.analyzingProgress);
// However, if we're being restored from a previous state, then don't
// create the fragment again (please)
if (savedInstanceState == null) {
firstFragment = new DirListFragment();
secondFragment = new GraphViewFragment();
getSupportFragmentManager().beginTransaction().add(R.id.fragment_container, secondFragment).commit();
fragments= new ArrayList<IFragment>();
fragments.add(firstFragment);
fragments.add(secondFragment);
}
}
#Override
public boolean onOptionsItemSelected(MenuItem item) {
if(item.getItemId() == R.id.listView){
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
/*transaction.hide(secondFragment);
transaction.show(firstFragment);*/
transaction.replace(R.id.fragment_container, firstFragment);
transaction.addToBackStack(null);
transaction.commit();
}
else if(item.getItemId() == R.id.graph){
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
/*transaction.hide(firstFragment);
transaction.show(secondFragment);*/
transaction.replace(R.id.fragment_container, secondFragment);
transaction.addToBackStack(null);
transaction.commit();
}
return true;
}
The problem comes when I try to access the fragment fields to update their data:
for(IMyFragment frag: fragments){
frag.updateData(data);
}
I get a NullPointerException after rotating the screen.
Also when switching fragmenents, their data is not updated (i.e. I start with list fragment, it's showing the proper data, then switch to graphic, no data is shown, switch back to list, no data again.)
If the fields are null when using setRetainInstance(true);, then you are recreating them (the fragments) in your activity. Make sure you are setting something in onSaveInstanceState() in your activity so in onCreate(), savedInstanceState will not be null, it can even be an empty bundle or useless variable, ex:
#Override
public void onSaveInstanceState(Bundle outstate){
super.onSaveInstanceState(outstate);
outstate.putBoolean("stateChanged", true);
}
Just make sure you call super before adding your arguments.
On another note, I personally have never stored my fragments in an array. I use a FrameLayout as a fragment container in my activities, just like you, and if I need to update something I just call: getSupportFragmentManager().findFragmentById(R.id.fragment_container).setWhatINeed(); This way I always get the visible fragment.
Let me know how the update goes and I'll update this post with any information I can.
Related
I am working on an application and there is one specific thing that is bothering me. Let's just say I have one activity and 2 fragments.FragmentA and FragmentB and FragmentA gets attached when activity starts.
I want to save the fragment data and fragment state when orientation changes occur.I have successfully saved fragment data using OnSavedInstanceState method. Now I want to save fragment state in the activity so that if orientation change occurs I want to be on the fragment I was (in my case either FragmentA or FragmentB depends on which was showing before config changes occur).
This is how I am saving the fragment state in the Activity:
#Override
protected void onSaveInstanceState(Bundle outState) {
// Save the values you need into "outState"
super.onSaveInstanceState(outState);
outState.putLong(SS_DATE, userDate.getTime());
android.support.v4.app.FragmentManager manager = getSupportFragmentManager();
Fragment currentFragment = this.getSupportFragmentManager().findFragmentById(R.id.content_container);
manager.putFragment(outState, "currentFragment", currentFragment);
}
And this is how I am retrieving on which fragment I was when the orientation change occurred:
#Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
FragmentManager manager = getSupportFragmentManager();
#SuppressLint("CommitTransaction")
FragmentTransaction transaction = manager.beginTransaction();
if (savedInstanceState != null) {
Fragment MyFragment = (Fragment) manager.getFragment(savedInstanceState, "currentFragment");
if (MyFragment instanceof FragListStudentsAttendance) {
Log.v("onRestore", FragListStudentsAttendance.TAG);
}else if (MyFragment instanceof FragGetClassesForAttendance){
Log.v("onRestore", FragGetClassesForAttendance.TAG);
if(MyFragment!=null) {
mFragGetClassesForAttendance = (FragGetClassesForAttendance) MyFragment;
}else{
mFragGetClassesForAttendance = new FragGetClassesForAttendance();
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
// mFragGetClassesForAttendanceNew.setRetainInstance(true);
// transaction.replace(R.id.content_ssadmin_container, mFragGetClassesForAttendanceNew, "FragGetClassesForAttendance").addToBackStack(null);
transaction.add(R.id.content_ssadmin_container, mFragGetClassesForAttendance, FragGetClassesForAttendance.TAG);
//transaction.replace(R.id.newEnrollmentMainContainer, mFragNewEnrollmentResults).addToBackStack("FragNewEnrollments");
transaction.commit();
mFragGetClassesForAttendance.setDate(userDate);
}
}
}
}
Now
Scenario 1:
If I am on fragment A and I rotate the device every thing works fine as it should. Like fragment have web services which loads the data into listview so I check if data exist then there is no need to run the web service and that working for now
Scenario 2:
If I am on fragment B and orientation change occurs everything works fine as it is supposed to be on fragment B. Now When I press back button Fragment A gets called again and all the data also comes from service. I think this shouldn't happen because it was supposed to be in BackStack and it's data was saved. So what Should I do now here?
Scenario 3: On FragmentB I have noticed that when I rotates the device the saveInstanceState function of FragmentA also gets called. Why it is so? where as I was replacing the FragmentB with FragmentA ?
Some Confusions:
Let me talk about some of the confusions also , maybe someone clear it to me although I have searched and read a lot about fragment and activity life cycle,
Actually I want to save the data per activity and fragment on device rotation. I know how to do it with activity(how to save states) so I also know how to do it in the fragment (save state of fragment views) now I am confused how to tell activity which fragment was showing and which to go after config changes(rotation) ? also what happens to FragmentA if I am on FragmentB Does its get attach and detach again and again in background?
I got your problems and confusions. I think the life cycle of fragment is confusing you. and indeed it will confuse you.
You need to learn different situations.
1. Fragment Life cycle when it is in foreground (attaching and detaching with activity) . Please keenly observe all the methods that will call i.e OnSaveInstance,onCreateView,OnDestroyView,onDestroy
2. Fragment life cycle when it is in background (observe the methods stated above)
3. Fragment life cycle when it is added to backstack (and not in foreground)
I am quite sure you are confused with the point number 3. As when the fragment is added to backstack it never gets destroy. So rotating device twice will set the ffragment data to null. I think you are restoring data on ActivityCreated or on onViewCreated ,
Ill suggest you to restore the fragment data in the oncreate. this will work for you when your fragment is coming back to foreground from the backstack .
Example
private List<String> mCountries;</pre>
#Override
public void onCreate(Bundle savedInstanceState)
{
if (savedInstanceState != null)
{
// Populate countries from bundle
}
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
View view = inflater.inflate(R.layout.fragment_countries, container, false);
if (mCountries == null)
{
// Populate countries by calling AsyncTask
}
return view;
}
public void onSaveInstanceState(Bundle outState)
{
// Save countries into bundle
}
Hope this will clear your confusions.
So basically what I'm working on is very similar to Instagram application, where there're a number of tabs and users can switch to any tab without any delay no matter what there's anything going on, such as refreshing, reloading, and etc. It also uses back button to go back to the previous preserved tab.
In order to achieve this, I've used FragmentManager with FragmentTransaction to show and hide each fragment which represents each tab. I didn't use replace or attach / detach because they destroy view hierarchy of previous tab.
My implementation works pretty well except that showing and hiding fragments are not committed (I highly doubt that this is a right word to say but so far that's how I understood the flow.), or don't occur immediately when SwipeRefreshLayout is refreshing on the fragment (to be hidden) which was added to FragmentManager later than the one to show.
My implementation follows the rules like these. Let's say we have 4 tabs and my MainActivity is showing the first tab, say FirstFragment, and the user selects the second tab, SecondFragment. Because SecondFragment had never been added before, I add it to FragmentManager by using FragmentTransaction.add and hide FirstFragment by using FragmentTransaction.hide. If the user selects the first tab again, because FirstFragment was previously added to FragmentManager, it doesn't add but only show FirstFragment and just hide SecondFragment. And selecting between these two tabs works smoothly.
But when the user "refreshes" SecondFragment's SwipeRefreshLayout and selects the first tab, FragmentTransaction waits for SecondFragment's refresh to be finished and commits(?) the actual transaction. The strange thing is that the transaction is committed immediately the other way around, from FirstFragment's refresh to SecondFragment.
Because this occurs by the order of addition to FragmentManager, I doubt that the order of addition somehow affects backstack of fragments and there might exists something like UI thread priority so that it forces the fragment transaction to be taken place after later-added fragment's UI transition finishes. But I just don't have enough clues to solve the issue. I've tried attach / detach and backstack thing on FragmentTransaction but couldn't solve the issue. I've tried both FragmentTransaction.commit and FragmentTransaction.commitAllowingStateLoss but neither solved the issue.
These are my MainActivity's sample code.
private ArrayList<Integer> mFragmentsStack; // This is simple psuedo-stack which only stores
// the order of fragments stack to collaborate
// when back button is pressed.
private ArrayList<Fragment> mFragmentsList;
#Override
protected void onCreate() {
mFragmentsStack = new ArrayList<>();
mFragmentsList = new ArrayList<>();
mFragmentsList.add(FirstFragment.newInstance());
mFragmentsList.add(SecondFragment.newInstance());
mFragmentsList.add(ThirdFragment.newInstance());
mFragmentsList.add(FourthFragment.newInstance());
mMainTab = (MainTab) findViewById(R.id.main_tab);
mMainTab.setOnMainTabClickListener(this);
int currentTab = DEFAULT_TAB;
mFragmentsStack.add(currentTab);
getSupportFragmentManager().beginTransaction().add(R.id.main_frame_layout,
mFragmentsList.get(currentTab), String.valueOf(currentTab)).commit();
mMainTab.setCurrentTab(currentTab);
}
// This is custom interface.
#Override
public void onTabClick(int oldPosition, int newPosition) {
if (oldPosition != newPosition) {
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
// First hide the old tab.
fragmentTransaction.hide(mFragmentsList.get(oldPosition));
// Recalculate the fragment stack.
if (mFragmentsStack.contains(newPosition)) {
mFragmentsStack.remove((Integer) newPosition);
}
mFragmentsStack.add(newPosition);
// Add new fragment if it's not added before, or show new fragment which was already hidden.
Fragment fragment = getSupportFragmentManager().findFragmentByTag(String.valueOf(newPosition));
if (fragment != null) {
fragmentTransaction.show(fragment);
} else {
fragmentTransaction.add(R.id.main_frame_layout, mFragmentsList.get(newPosition),
String.valueOf(newPosition));
}
// Commit the transaction.
fragmentTransaction.commitAllowingStateLoss();
}
}
// It mimics the tab behavior of Instagram Android application.
#Override
public void onBackPressed() {
// If there's only one fragment on stack, super.onBackPressed.
// If it's not, then hide the current fragment and show the previous fragment.
int lastIndexOfFragmentsStack = mFragmentsStack.size() - 1;
if (lastIndexOfFragmentsStack - 1 >= 0) {
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.hide(mFragmentsList.get(mFragmentsStack.get(lastIndexOfFragmentsStack)));
fragmentTransaction.show(mFragmentsList.get(mFragmentsStack.get(lastIndexOfFragmentsStack - 1)));
fragmentTransaction.commitAllowingStateLoss();
mMainTab.setCurrentTab(mFragmentsStack.get(lastIndexOfFragmentsStack - 1));
mFragmentsStack.remove(lastIndexOfFragmentsStack);
} else {
super.onBackPressed();
}
}
Just faced the same issue with only difference - I'm switching fragments on toolbar buttons click.
I've managed to get rid of overlapping fragments overriding onHiddenChanged:
#Override
public void onHiddenChanged(boolean hidden) {
super.onHiddenChanged(hidden);
if (hidden) {
yourSwipeRefreshLayout.setRefreshing(false);
}
}
I have a fragment (say FragA) gets and another fragment (say FragB).
In FragA I set some values seekbar and put a check on checkbox
but now if click on FragB and then click on FragA then all set values are gone.
For fragments I have used a ListAdapter which set a fragment depending on position no. where I clicked. I don't want it on back button press but on clicking that fragment.
My Question is how can I restore a fragment to its previous state while setting parameter in other fragments and clicking to those fragments which I set before.
Make use of onSaveInstanceState(), below is the example how you need to implement it
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt("curChoice", mCurCheckPosition);
}
And later you can use it like this:
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (savedInstanceState != null) {
// Restore last state for checked position.
mCurCheckPosition = savedInstanceState.getInt("curChoice", 0);
}
}
It will be a good idea if you go through fragments documentation
You can save each transaction to a back stack managed by the activity, allowing the user to navigate backward through the fragment changes (similar to navigating backward through activities).
FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
Before you call commit(), however, you might want to call addToBackStack(), in order to add the transaction to a back stack of fragment transactions. This back stack is managed by the activity and allows the user to return to the previous fragment state, by pressing the Back button.
detail
Is it possible to keep a Fragment running while I call replace from FragmentManager to open a new Fragment?
Basically I don't want to pause a Fragment when navigating (via replace method) to another Fragment.
Is it possible?
Or the correct approach is, always, instantiate a new Fragment every time I need to open it and restore its previous state?
Thanks!
FragmentManger replace method will destroy the previous fragment completely, So in each transaction onDestroyView(), onDestroy() and onDetach() will get called on previous fragment. If you want to keep your fragment running you can instead use FragmentManger hide() and show() methods! It hides and shows the fragments without destroying them.
so first add both fragments to fragment manager and also hide the second fragment.
fragmentManager.beginTransaction()
.add(R.id.new_card_container, FragmentA)
.add(R.id.new_card_container,FragmentB)
.hide(FragmentB)
.commit();
Note that you can only call show() on hidden fragment. So here you can't call show() on FragmentA but it's not a problem because by hiding and showing FragmentB you can get replacement effect you want.
And here is a method to go back and forth between your fragments.
public void showOtherFragment() {
if(FragmentB.isHidden()){
fragmentManager.beginTransaction()
.show(FragmentB).commit();
} else {
fragmentManager.beginTransaction()
.hide(FragmentB).commit();
}
}
Now if you put log message in fragment callback method you will see there is no destruction (except for screen orientation change!), even view will not get destroyed since onDistroyView doesn't get called.
There is only one problem and that is, first time when application starts onCreateView() method get called one time for each fragment (and it should be!) but when the orientation changes onCreateView() gets called twice for each fragment and that's because fragments once created as usual and once because of there attachment to FragmentManger (saved on bundle object) To avoid that you have two options 1) detach fragments in onSaveInstaneState() callback.
#Override
protected void onSaveInstanceState(Bundle outState) {
fragmentManager.beginTransaction()
.detach(FragmentA)
.detach(FragmentB)
.commit();
super.onSaveInstanceState(outState);
}
It's working but view state will not get updated automatically, for example if you have a EditText its text will erase each time orientation change happens. of course you can fix this simply by saving states in the fragment but you don't have to if you use the second option!
first i save a Boolean value in onSaveInstaneState() method to remember witch fragment is shown.
#Override
protected void onSaveInstanceState(Bundle outState) {
boolean isFragAVisible = true;
if(!FragmentB.isHidden())
isFragAVisible = false;
outState.putBoolean("isFragAVisible",isFragAVisible);
super.onSaveInstanceState(outState);
}
now in activity onCreate method i check to see if savedInstanceState == null. if yes do as usual if not activity is created for second time. so fragment manager already contains the fragments. So instead i'm getting a reference to my fragments from fragment manager. also i make sure correct fragment is shown since its not recovered automatically.
fragmentManager = getFragmentManager();
if(savedInstanceState == null){
FragmentA = new FragmentA();
FragmentB = new FragmentB();
fragmentManager.beginTransaction()
.add(R.id.new_card_container, FragmentA, "fragA")
.add(R.id.new_card_container, FragmentB, "fragB")
.hide(FragmentB)
.commit();
} else {
FragmentA = (FragmentA) fragmentManager.findFragmentByTag("fragA");
FragmentB = (FragmentB) fragmentManager.findFragmentByTag("fragB");
boolean isFragAVisible = savedInstanceState.getBoolean("isFragAVisible");
if(isFragAVisible)
fragmentManager.beginTransaction()
.hide(FragmentB)
.commit();
else
fragmentManager.beginTransaction()
.hide(FragmetA) //only if using transaction animation
.commit();
}
By now your fragment will work perfectly if are not using transaction animation. If you do, you also need to show and hide FragmentA. So when you want to show FragmentB first hide FragmentA then show FragmentB (in the same transaction) and when you want to hide FragmentB hide it first and also show FragmentA (again in the same transaction). Here is my code for card flip animation (downloaded from developer.goodle.com)
public void flipCard(String direction) {
int animationEnter, animationLeave;
if(direction == "left"){
animationEnter = R.animator.card_flip_right_in;
animationLeave = R.animator.card_flip_right_out;
} else {
animationEnter = R.animator.card_flip_left_in;
animationLeave = R.animator.card_flip_left_out;
}
if(cardBack.isHidden()){
fragmentManager.beginTransaction()
.setCustomAnimations(animationEnter, animationLeave)
.hide(cardFront)
.show(cardBack)
.commit();
} else {
fragmentManager.beginTransaction()
.setCustomAnimations(animationEnter,animationLeave)
.hide(cardBack)
.show(cardFront)
.commit();
}
}
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.