I have a FragmentActivity which shows some fragments by ViewPager. Here's my code snippet,
MyFragmentActivity
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity);
viewPager = (ViewPager) findViewById(R.id.pager);
fetchDataAsync(this);
}
#Override
private void onDataReceived(List<String> data) {
viewPager.setAdapter(new MyFragmentPageAdapter(getChildFragmentManager()), data);
}
MyFragmentPagerAdapter
private class MyFragmentPagerAdapter extends FragmentPagerAdapter {
private Data data;
public MyFragmentPagerAdapter(FragmentManager fm, Data data)
{
super(fm);
this.data = data;
}
#Override
public Fragment getItem(int position) {
return MyFragment.newInstance(data.get(position));
}
#Override
public int getCount() {
return data.size();
}
#Override
public CharSequence getPageTitle(int position) {
return null;
}
}
The fragments are created dynamically by FragmentPagerAdapter and depend on data received.
My app crashed when screen rotation happened. With some search, I learned that "When a config change occurs the old Fragment isn't destroyed - it adds itself back to the activity when it's recreated". At that moment the data was null for recreated fragment so it's crashed at onCreateView.
Someone said FragmentStatePagerAdapter should work but not for my case. Another said reusing the fragment instead of recreating new ones, but ViewPager creates them automatically and how can I manage this. I want to permanently destroy the old fragments and not re-add them to activity, what could I do?
UPDATED
I removed constructor param from MyFragment, and passed it by setArgument
Bundle b = new Bundle();
b.putString("data", data.toString());
f.setArguments(b);
Now fragment can restore data while recreating since MyFragment could save state(data) in this way when config changes happened
You should handle the orientation change yourself.
It is covered by developer resources under configuration change, basically, this means adding to your AndroidManifest the attribute configChanges="orientation". Then when orientation changes, Activity.onConfigurationChanged will be called.
See for example Activity under Configuration Changes
Normally, this manual handle will result in your fragment being laid out, and so its size should change automatically.
Another solution, is to keep it like this : i mean, on orientation change, your
Activity is being recreated, and so is your Fragment.
So you need to ensure in your fragment that the data are available, eventually loading them if not.
Related
I'm having some problems when dealing with Fragment using ViewPager.
What I'm having:
An activity (say, MainActivity) that contains a ViewPager to display some Fragment(s). Some of them contains a callback interface, which will be called to do somethings in the MainActivity.
The MainActivity has a FragmentPagerAdapter class, which is used as the adapter of the ViewPager. And a List<Fragment> in FragmentPagerAdapter to store some Fragment that will be displayed on the ViewPager.
What I'm expecting:
First launch, the Fragment called the callback interface's methods when I hit a button in it and MainActivity did somethings inside that. It worked great.
After a screen rotation, I expected it to work the same as the first launch BUT
NullPointerException: attempt to invoke a method on a null reference object (particularly, the Fragment's interface) hit me in my face.
What I know:
- The getItem(int position) won't be called again once the Fragment is created. Instead the instantiateItem(ViewGroup container, int position) will be called.
- FragmentManager will store some Fragment in mActive.mValues
- ViewPager and fragments — what's the right way to store fragment's state? (I did reference to this and some other same topics on StackOverflow too.)
What I have tried and saw:
- Override instantiateItem(ViewGroup container, int position)
- Debugged for 1 day. I saw that when I pass getSupportFragmentManager() in MainActivity's onCreate() method to FragmentPagerAdapter's super constructor, in the first launch, it has an "address in memory", assume it was '1a1a1a1'. The mActive.mValues of FragmentManager saved some Fragment' "address in memory" which are identical to the List<Fragment> containing them (assume it was 'qwertyu'). Which meaned it was right.
But when I rotated the screen, passing the getSupportFragmentManager() again, the "address in memory" was completely different, assume '9f9f9f9'. And FragmentManager's mActive.mValues contained a different set of Fragment' "address in memory" too (assume 'abcdeff'), although the number of Fragment in it was equal to the number of Fragment that was saved on the first launch (before rotation).
I have added a Fragment to the List<Fragment> with a new "address in memory" (assume 'abababa'), has the callback interface. But when I hit the button in it, it was the Fragment that was in the FragmentManager's mActive.mValues after the rotation (with "address in memory" is 'abcdeff' as I assumed above), and that one didn't have the callback interface (due to not being set in MainActivity first). And caused the NullPointerException as mentioned above.
My questions now is:
- First of all, how to get rid of this problem!? It would be better to keep using FragmentPagerAdapter instead of another class. But I will consider using other class too.
- Second, can you explain why FragmentManager saved the Fragment instance before rotation. But after rotation, it creates a completely different Fragment instance but still uses it instead of the Fragment that was saved in the List<Fragment>?
Here is the code (I think I didn't use the instantiateItem(ViewGroup container, int position) method in the right way so it still caused the problem).
public class MainActivity extends AppCompatActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//Attach the SectionsPagerAdapter to the ViewPager
SectionsPagerAdapter pagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
ViewPager viewPager = findViewById(R.id.viewPager);
viewPager.setAdapter(pagerAdapter);
}
//
//
//Adapter class
private class SectionsPagerAdapter extends FragmentPagerAdapter {
private static final int PAGE_HOME = 0;
private int tabCount = 1;
private List<Fragment> fragmentList = new ArrayList<>();
private List<String> fragmentTitleList = new ArrayList<>();
//private FragmentManager fragmentManager;
SectionsPagerAdapter(FragmentManager fm) {
super(fm);
//fragmentManager = fm;
//Default HomeFragment
HomeFragment homeFragment = new HomeFragment();
//Callback interface
homeFragment.setOnCategoryFragmentChangedListener(new HomeFragment.OnCategoryFragmentChangedListener() {
//This method will be called when a button in HomeFragment is clicked
#Override
public void onAddNewCategory(String categoryName) {
addNewCategory(categoryName);
}
});
fragmentList.add(homeFragment);
fragmentTitleList.add("Home");
}
#Override
public Fragment getItem(int position) {
return fragmentList.get(position);
}
#Override
public int getCount() {
return tabCount;
}
#Override
public CharSequence getPageTitle(int position) {
return fragmentTitleList.get(position);
}
#Override
public Object instantiateItem(ViewGroup container, int position) {
fragmentList.set(position, (Fragment) super.instantiateItem(container, position));
return fragmentList.get(position);
}
private void addNewCategory(String categoryName) {
CategoryFragment fragment = new CategoryFragment();
tabCount += 1;
fragmentList.add(fragment);
fragmentTitleList.add(categoryName);
notifyDataSetChanged();
}
}
}
Please help. I'm being insane for 2 days now...!
I believe android restarts the activity during orientation change thus making multiple instances of FragmentPagerAdapter and multiple set of instances of List.
I don't completely understand your question but I suspect instantiateItem doesn't do anything anyway. Doesn't the Fragment getItem(int pos) work without overriding instantiateItem()?
Oh well, right after i felt in sleep, I found the solution. It's true that I didn't use the instantiateItem() method in the right way. After debugging again, I found that the instantiateItem() method get call whenever I swipe (or choose if using TabLayout as well) to another Fragment, and even get call before getItem(int pos), no matter what it's the first launch or after rotation. Which is why I think we should set things up for the Fragment in the instantiateItem() method.
So here is how I use the instantiateItem() method now:
#Override
public Object instantiateItem(ViewGroup container, int position) {
fragmentList.set(position, (Fragment) super.instantiateItem(container, position));
Fragment fragment = fragmentList.get(position);
if (position == PAGE_HOME) {
((HomeFragment) fragment).setOnCategoryFragmentChangedListener(new HomeFragment.OnCategoryFragmentChangedListener() {
#Override
public void onAddNewCategory(String categoryName) {
addNewCategory(categoryName);
}
});
}
return fragment;
}
If anyone have a better solution, please just tell me if you don't mind. I will consider about it.
I am having problems with fragments or maybe rather the way I want them to work.
In my activity I have a layout container (R.id.container1) to which I add a fragment (FragMain) programmatically at startup.
In the fragment I have 2 Fragments (Frag1, Frag2) and a ViewPager which loads several other fragments (FragA, FragB, FragC, FragD, FragE) via a FragmentPagerAdapter.
On a button press I replace the whole content of R.id.container1, so FragMain is replaced with another Fragment (FragSub).
Now when I press the back button, FragMain is loaded again, but the ViewPager isn't fully initialized, some Fragments are missing. From what I observed, it's always FragB and FragD missing, If I scroll to the empty views, the app crashes. The other fragments seem to be fine. What's going wrong here?
This thread suggests using getChildFragmentManager in creation of the PagerAdapter, but I am already doing that... Fragment in ViewPager not restored after popBackStack
I am accessing the fragments via e.g.
((FragA)((PagerAdapter)viewPager.getAdapter()).getItem(0));
Is this the best way?
Some Code :
MainActivity's onCreate
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
pagerFragList = new Vector<Fragment>();
pagerFragList.add(Fragment.instantiate(this, FragA.class.getName()));
pagerFragList.add(Fragment.instantiate(this, FragB.class.getName()));
pagerFragList.add(Fragment.instantiate(this, FragC.class.getName()));
pagerFragList.add(Fragment.instantiate(this, FragD.class.getName()));
pagerFragList.add(Fragment.instantiate(this, FragE.class.getName()));
FragMain fragMain = (FragMain)getSupportFragmentManager().findFragmentByTag(FragMain.debugTag);
if(fragMain==null) fragMain = new FragMain();
FragmentHelper.replaceSupportFragment(this, fragMain, R.id.container1, false, FragMain);
}
Helper
public Vector<Fragment> getFragList() {
return pagerFragList;
}
FragMain's onViewCreated
#Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
pagerAdapter = new PagerAdapter(getChildFragmentManager(), ((MainActivity)getActivity()).getFragList());
viewPager = ((ViewPager)view.findViewById(R.id.viewPager));
viewPager.setAdapter(pagerAdapter);
viewPager.setOffscreenPageLimit(5);
viewPager.setCurrentItem(0, true);
}
The PagerAdapter
public class PagerAdapter extends FragmentPagerAdapter {
private List<Fragment> fragments;
public PagerAdapter(FragmentManager fm, List<Fragment> fragments) {
super(fm);
this.fragments=fragments;
}
#Override
public Fragment getItem(int position) {
return fragments.get(position);
}
#Override
public int getCount() {
return fragments.size();
}
}
Have no idea why this seems to be fixing it on my end, but try commenting out your "viewPager.setOffscreenPageLimit(5);" line.
I'm still investigating and will be back with more if I find anything else useful out.
The other thing it turns out that I had to do was override the "state" methods of a FragmentStatePagerAdapter like this:
#Override
public Parcelable saveState() {
return null;
}
#Override
public void restoreState(Parcelable state, ClassLoader loader) {
//do nothing
}
In order to bypass whatever strange thing that was causing problems (must have had something to do with saving and restoring the states of nested fragments in a ViewPager...
There are alot of permutations involving the state of fragments and backstack transitions. For debug, it may help to isolate the event you are working with (backbutton , orientation change ... ) and to look at the following before and after your event....
state of fragment manager which includes the backstack i believe...
//debug
getFragmentManager().dump("", null,
new PrintWriter(System.out, true), null);
see this thread
As viewpager preload +1 and -1 of the fragment that is used in it. I've a loading of data using asyntask that i wish to execute only when i am on the page itself.
Where should i execute the function? oncreate does not seems to work for me
You can implement ViewPager.OnPageChangeListener and run your AsyncTask in onPageSelected().
For example:
public class MyActivity implements ViewPager.OnPageChangeListener {
#Override
public void onPageSelected(int position) {
new MyAsyncTask().execute();
}
}
However, as Tyczj pointed out in the comments, this defeats the purpose of a ViewPager trying to keep Views loaded. This feature is designed to make your app look smooth, and without it your Views will look empty (or take on their default appearance) while you load your data.
The solution provided by Tanis should work, there's however one think that should be taken in consideration. Since the AsyncTask is started from Activity, you may encounter some issues when dealing with configuration changes. Perhaps starting the AsyncTask dirrectly from the fragment will make more sense.
The solution then would be to make the currently displayed fragment aware that he is the fragment displayed now.
Firstly, you should have a method in your Activity that will return the position of current fragment from ViewPager:
public class MainActivity extends Activity{
//....
public int getViewPagerCurrentIndex() {
return pager.getCurrentItem();
}
}
Secondly, in your PagerAdapter in getItem() method pass the position of current item as an argument to the fragment:
public class MyPagerAdapter extends FragmentStatePagerAdapter {
//....
#Override
public Fragment getItem(int position) {
return MyFragment.newInstance(position);
}
}
Lastly, check in MyFragment that both position returned from MainActivity and position received when the fragment was instantiated match. If they match then this instance of fragment is visible:
public static MyFragment newInstance(int position) {
MyFragment fragment = new MyFragment ();
Bundle args = new Bundle();
args.putInt(KEY_POSITION, position);
fragment.setArguments(args);
return fragment;
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
if (getArguments() != null) {
fragmentPosition = getArguments().getInt(KEY_POSITION);
}
}
// It does not matter where this method is called, the AsyncTask will be started only for the currently visible fragment.
private void executeAsyncTask(){
MainActivity mainActivity = (MainActivity)getActivity();
if (mainActivity.getViewPagerCurrentIndex() == fragmentPosition ) {
new MyAsyncTask().execute();
}
}
I have following issue. I have ViewPager and FragmentPagerAdapter for it.
All I need to do is call some method from my Fragment which is in ViewPager when this page was selected.
I tried to keep all my Fragments in List inside my adapter, but the problem is that when I rotate my device Adapter use previous fragments. New fragments were created after rotation and I have list of them, but I don't now how to get access to previous fragments. I have references to new fragments only.
Here is my adapter:
public class MainPagerAdapter extends FragmentPagerAdapter {
private List<Fragment> fragmentList;
public MainPagerAdapter(FragmentManager fm) {
super(fm);
fragmentList = new ArrayList<Fragment>();
fragmentList.add(new TaskPageFragment());
fragmentList.add(new HistoryFragment());
fragmentList.add(new TestFragment());
}
#Override
public Fragment getItem(int i) {
return fragmentList.get(i);
}
#Override
public int getCount() {
return 3;
}
}
Of course when I trying to access to fragment which was only created like an object, I can't get access to Activity Context.
Fragment fragment = pagerAdapter.getItem(i);
if (fragment instanceof SelectionListener) {
((SelectionListener)fragment).onTabSelected();
}
Here is onTabSelected() method call is shown. It's my interface which I implemented to each fragment in ViewPager and when it gets called after screen rotation I get null Context
Device configurations can change during runtime (such as screen orientation). When this happens the Activity is restarted. This is done so your activities can adjust to match the new device configuration, and since fragments are within Activities they are also 'destroyed'.
However your issue may be solved with something simple, since from what I understand from your question you are not looking for UI configuration changes and assuming you are using API 11+.
public void setRetainInstance (boolean retain)
Since: API Level 11
Control whether a fragment instance is retained across Activity re-creation (such as from a configuration change). This can only be used with fragments not in the back stack. If set, the fragment lifecycle will be slightly different when an activity is recreated:
onDestroy() will not be called (but onDetach() still will be, because
the fragment is being detached from its current activity).
onCreate(Bundle) will not be called since the fragment is not being
re-created.
onAttach(Activity) and onActivityCreated(Bundle) will
still be called.
public MainPagerAdapter(FragmentManager fm) {
Fragment frag = null;
super(fm);
fragmentList = new ArrayList<Fragment>();
frag = new TaskPageFragment();
frag.setRetainInstance(true);
fragmentList.add(frag);
frag = new HistoryFragment();
frag.setRetainInstance(true);
fragmentList.add(frag);
frag = new TestFragment();
frag.setRetainInstance(true);
fragmentList.add(frag);
}
This question already has answers here:
ViewPager and fragments — what's the right way to store fragment's state?
(11 answers)
Closed 7 years ago.
I'have 1 FragmentActivity with a ViewPager which handle 2 Fragments.
public class MyFragmentActivity extends FragmentActivity{
private Fragment f1;
private Fragment f2;
private ViewPager myPager;
private MyFragmentAdapter mFragmentsAdapter;
private static ArrayAdapter<Fragment> mFragmentArray;
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
setContentView(R.layout.my_layout);
myViewPager = (ViewPager) findViewById(R.id.pager_acciones);
mFragmentArray = new ArrayAdapter<Fragment>(getApplicationContext(),android.R.layout.simple_list_item_1);
f1 = new Fragment();
f1 = new Fragment();
mFragmentArray.add(f1);
mFragmentArray.add(f2);
mFragmentsAdapter = new MyFragmentAdapter(getSupportFragmentManager());
myPager.setAdapter(mFragmentsAdapter);
myPartidoPager.setCurrentItem(0);
}
public static class MyFragmentAdapter extends FragmentPagerAdapter {
public AccionesFragmentAdapter(FragmentManager fm) {
super(fm);
}
#Override
public int getCount() {
return mFragmentArray.getCount();
}
#Override
public Fragment getItem(int position) {
return mFragmentArray.getItem(position);
}
}
My problem is that every time the screen orientation changes, the activity is created and also the Fragments. I don't mind if the activity is created again, but I don't want the Fragments to be recreated.
The easiest way is to throw a if statement right after the on create method is called and check to see if you already have something in there. If savedInstanceState is not null, you don't need to do anything:
if (savedInstanceState == null){
// do whatever
} else {
// dont do anything
}
(this answer is more for people that stumble upon this question cause they can't figure out why fragments get re-added every time they turn their device)
You could add:
android:configChanges="screenSize|orientation"
In AndroidManifest.xml.
This will prevent Android calling onCreate on screen orientation change. If you want to perform special handling of orientation change, you can override onConfigurationChanges.
Save your FragmentAdapter in onSaveInstanceState and return it to the Activity after a configuration change with onRetainNonConfigurationInstance so the fragments survive an orientation change.