weakreferences of fragments on Orientation Change using FragmentStatePagerAdapter -Android - android

Im keeping track of fragments in a fragmentstatepageradapter using weakreferences to the fragments. I found it cheaper then making a vector to keep track of the actual fragments. So then i could call a function called getFragmentt(2) for example and it would search my list of weakreferences for fragment and if its alive it will return the real object. Alls well so far. Now if i rotate the device the fragmentManager recreates the fragments but the weakreferences are all lost as a result. The adapter does not even get called when the fragments are getting re-created on a orientation change. Here is the code.
public class SectionsPagerAdapter extends FragmentStatePagerAdapter{
ArrayList<WeakReference<Fragment>> m_fragments;
public SectionsPagerAdapter(FragmentManager fm) {
super(fm);
m_fragments=new ArrayList<WeakReference<Fragment>>();
}
#Override
public Fragment getItem(int position) {
// getItem is called to instantiate the fragment for the given page.
// Return a listFragment ) with the page number as one of its argument.
TweetsListFragment mListfragment = TweetsListFragment.newInstance(
position, nf, mpageTitles[position]);
m_fragments.add(position, new WeakReference<Fragment>(mListfragment));
return mListfragment;
}
#Override
public int getItemPosition(Object object) {
printLog(Consts.TAG, "getting item position from pageview adapter");
return PagerAdapter.POSITION_NONE;
}
#Override
public int getCount() {
return mpageTitles.length;
}
#Override
public CharSequence getPageTitle(int position) {
return mpageTitles[position];
}
/*return actual object from weak store of fragments on request*/
public Fragment getFragment(final int position) {
return m_fragments.get(position) == null ? null :
m_fragments.get(position).get();
}
......... etc

Related

Tabbed fragments list views are not updated when related data is changed in other fragment

I used a TabLayout for a tabbed view in my app. I added three List fragments - one for each tab.
The content of either of the fragments is supposed to be changed (and the list views have to be updated) when new record is added/removed from any of the fragments.
But after I modify some data and swiped to the next tab the list view is not updated and retained the old data.
I created an interface that is implemented in all of the ListFragments as follows:
public interface UpdatableFragment {
void updateContent();
}
public class LogFragment implements UpdatableFragment{
...
#Override
public void updateContent() {
Activity activity = getActivity();
if (isAdded() && activity != null) {
getLoaderManager().restartLoader(LOG_LIST_LOADER, null, this);
}
}
...
}
And I call the method in the MainActivity tab select listener
tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
#Override
public void onTabSelected(TabLayout.Tab tab) {
if(adapter != null){
int position = viewPager.getCurrentItem();
viewPager = (ViewPager) findViewById(R.id.viewpager);
viewPager.setCurrentItem(position);
Fragment tabbedFragment = adapter.getItem(viewPager.getCurrentItem());
if(tabbedFragment instanceof UpdatableFragment){
((UpdatableFragment) tabbedFragment).updateContent();
}
}
}
But the condition if (isAdded() && activity != null) in updateContent() method sometimes returns false because activity becomes null. On other times, it returns the MainActivity instance and the loader restarts.
Why is this inconsistent behavior happening and how can I make the ListFragments always show fresh content when their corresponding tabs are selected?
EDIT
adding the fragment pager code
class ViewPagerAdapter extends FragmentPagerAdapter {
private final List<Fragment> mFragmentList = new ArrayList<>();
private final List<String> mFragmentTitleList = new ArrayList<>();
public ViewPagerAdapter(FragmentManager manager) {
super(manager);
}
#Override
public Fragment getItem(int position) {
return mFragmentList.get(position);
}
#Override
public int getCount() {
return mFragmentList.size();
}
public void addFragment(Fragment fragment, String title) {
mFragmentList.add(fragment);
mFragmentTitleList.add(title);
}
#Override
public CharSequence getPageTitle(int position) {
return mFragmentTitleList.get(position);
}
}
The getItem(int position) method does not return a reference to the Fragment currently in that position of your ViewPager. The getItem() method is used in the PagerAdapter when it calls instantiateItem() to create your views/fragments, so when you call getItem(int position) it's creating a brand new Fragment for you, and this is not added/attached to your Activity (causing the false/null in your if check). If you look into the code for FragmentPagerAdapter.instantiateItem() you can see where it calls getItem() and then adds the Fragment with the FragmentManager.
One way to get around this is to cache your Fragment references in your FragmentPagerAdapter (do this by caching the Fragment returned by instantiateItem() and remove the references in adapter.destroyItem()), or calling adapter.notifyDataSetChanged() to re-create your views with the updated data.
UPDATE:
You want to add something like this to your FragmentPagerAdapter:
public class Adapter extends FragmentPagerAdapter {
private SparseArray<Fragment> fragmentCache = new SparseArray<>();
#Override public Object instantiateItem(ViewGroup container, int position) {
Fragment fragment = (Fragment) super.instantiateItem(container, position);
fragmentCache.put(position, fragment);
return fragment;
}
#Override public void destroyItem(ViewGroup container, int position, Object object) {
fragmentCache.remove(position);
super.destroyItem(container, position, object);
}
// other code....
}
You can then get references to all the fragments currently in your adapter from the fragmentCache, and then you can call your updateContent method on them.

Dynamically remove an item from a ViewPager with FragmentStatePagerAdapter

There are quite a few discussions around this topic
ViewPager PagerAdapter not updating the View
Update ViewPager dynamically?
Removing fragments from FragmentStatePagerAdapter
I have tried various solutions (including the invalidation with POSITION_NONE)
. But I still donT know how to remove an item properly.
What happens is
either I get a blank page (meaning the fragment is destroyed, but the
instantiateItem was not called for a replacement)
or the whole thing crashes probably because the way the Android manages the
fragment instances do not match how I keep them in my
arraylist/sparsearray
Here s my adapter
private class DatePickerPagerAdapter extends FragmentStatePagerAdapter {
ArrayList<Fragment> registeredFragments = new ArrayList<Fragment>();
public DatePickerPagerAdapter(FragmentManager fm) {
super(fm);
}
#Override
public Fragment getItem(int position) {
return CreateWishFormDatePaginationFragment.newInstance(position);
}
#Override
public int getItemPosition(Object object){ //doesnt change much..
return PagerAdapter.POSITION_NONE;
}
#Override
public int getCount() {
return iPageCount;
}
#Override
public Object instantiateItem(ViewGroup container, int position) {
Fragment fragment = (Fragment) super.instantiateItem(container, position);
registeredFragments.add(position, fragment);
return fragment;
}
#Override
public void destroyItem(ViewGroup container, int position, Object object) {
super.destroyItem(container, position, registeredFragments.get(position));
}
public void removePage(ViewGroup pager, int position) {
destroyItem(pager, position, null);
registeredFragments.remove(position);
iPageCount--;
pagerIndicator.notifyDataSetChanged();
pagerAdapter.notifyDataSetChanged();
}
public void addPage() {
iPageCount++;
pagerIndicator.notifyDataSetChanged();
pagerAdapter.notifyDataSetChanged();
}
}
I am using a view pager with ViewPagerIndicator and I want to be able to remove a page in between, for example.
Hence remains the question, what is the proper way handling addition and removal of fragments in a ViewPager?
Thanks!
If you want to remove items from a ViewPager, this following code does not make sense:
#Override
public Fragment getItem(int position) {
return CreateWishFormDatePaginationFragment.newInstance(position);
}
Essentially, you create a Fragment based on the position. No matter which page you remove, the range of the position will change from [0, iPageCount) to [0, iPageCount-1), which means that it will always get rid of the last Fragment.
What you need is more or less the following:
public class DatePickerPagerAdapter extends FragmentStatePagerAdapter
{
private ArrayList<Integer> pageIndexes;
public DatePickerPagerAdapter(FragmentManager fm) {
super(fm);
pageIndexes = new ArrayList<>();
for (int i = 0; i < 10; i++) {
pageIndexes.add(new Integer(i));
}
}
#Override
public int getCount() {
return pageIndexes.size();
}
#Override
public Fragment getItem(int position) {
Integer index = pageIndexes.get(position);
return CreateWishFormDatePaginationFragment.newInstance(index.intValue());
}
// This is called when notifyDataSetChanged() is called
#Override
public int getItemPosition(Object object) {
// refresh all fragments when data set changed
return PagerAdapter.POSITION_NONE;
}
// Delete a page at a `position`
public void deletePage(int position)
{
// Remove the corresponding item in the data set
pageIndexes.remove(position);
// Notify the adapter that the data set is changed
notifyDataSetChanged();
}
}
Please refer to this complete example for more details about removing item from FragmentStatePagerAdapter.
The ViewPager doesn't remove your fragments with the code above because it loads several views (or fragments in your case) into memory. In addition to the visible view, it also loads the view to either side of the visible one. This provides the smooth scrolling from view to view that makes the ViewPager so cool.
To achieve the effect you want, you need to do a couple of things.
Change the FragmentPagerAdapter to a FragmentStatePagerAdapter. The reason for this is that the FragmentPagerAdapter will keep all the views that it loads into memory forever. Where the FragmentStatePagerAdapter disposes of views that fall outside the current and traversable views.
Override the adapter method getItemPosition (shown below). When we call mAdapter.notifyDataSetChanged(); the ViewPager interrogates the adapter to determine what has changed in terms of positioning. We use this method to say that everything has changed so reprocess all your view positioning.
And here's the code...
private class MyPagerAdapter extends FragmentStatePagerAdapter {
//... your existing code
#Override
public int getItemPosition(Object object){
return PagerAdapter.POSITION_NONE;
}
}

How to add Fragment to first position in FragmentPagerAdapter

I am using FragmentPagerAdapter and a ViewPager to add custom Fragments EDIT: from my MainActivity (also sending a bunch of extra data based on a JSON response via bundle) and using swiping motions to move to the next Fragments in the List.
public class MyPagerAdapter extends FragmentPagerAdapter implements Serializable {
public List<Fragment> fragments;
public FragmentManager fm;
public MyPagerAdapter(FragmentManager fm) {
super(fm);
this.fm = fm;
this.fragments = new ArrayList<Fragment>();
}
#Override
public Fragment getItem(int position) {
return fragments.get(position);
}
#Override
public int getCount() {
return fragments.size();
}
}
Everything is working fine as long as I'm adding new Fragments by
using
MyPagerAdapter pageAdapter = new MyPagerAdapter(getSupportFragmentManager());
pager = (ViewPager)findViewById(R.id.myViewPager);
pageAdapter.fragments.add(new CustomFragment());
pager.setAdapter(pageAdapter);
But I can't find a proper way to add Fragments to the beginning of the List and swipe back.
I've tried both
pageAdapter.fragments.add(0, new CustomFragment());
as well as changing the FragmentPagerAdapters List to LinkedList and using
pageAdapter.fragments.addFirst(new CustomFragment());
and then refreshing the adapter by using
pageAdapter.notifyDataSetChanged();
and i keep getting the following exception:
java.lang.IllegalStateException: Can't change tag of fragment CustomFragment{2ead9520 #10 id=0x7f0a0002 android:switcher:2131361794:10}: was android:switcher:2131361794:10 now android:switcher:2131361794:11
The key methods no one has talked about yet is public int getItemPosition(Object) which is used to remap fragments to pages after they move and public long getItemId(int position) which must be overridden by a pager adapter that reorders fragments. The default implementation uses the position of the page as the id, so reordering confuses the FragmentPagerAdapter.
(I am leaving out the Serializable interface as it is irrelevant for the purposes of answering the question - How to reorder fragments in a FragmentPagerAdapter).
public class MyPagerAdapter extends FragmentPagerAdapter {
public List<Fragment> fragments;
public FragmentManager fm;
public MyPagerAdapter(FragmentManager fm) {
super(fm);
this.fm = fm;
this.fragments = new ArrayList<Fragment>();
}
void addFragmentAtPosition(int position, Fragment f) {
if(position == fragments.size())
fragments.add(f);
else
fragments.add(position, f);
notifyDataSetChanged();
}
void removeFragmentAtPosition(int position) {
Fragment f = fragments.remove(position);
if(f != null)
fm.beginTransaction().remove(f).commit();
notifyDataSetChanged();
}
#Override
public Fragment getItem(int position) {
return fragments.get(position);
}
#Override
public int getCount() {
return fragments.size();
}
#Override
public int getItemPosition(Object object){
/*
* called when the fragments are reordered to get the
* changes.
*/
int idx = fragments.indexOf(object);
return idx < 0 ? POSITION_NONE : idx;
}
#Override
public long getItemId(int position) {
/*
* map to a position independent ID, because this
* adapter reorders fragments
*/
return System.identityHashCode(fragments.get(position));
}
}
The key additions are the overrides of public int getItemPosition(Object) and public long getItemId(int). These allow the FragmentPagerAdapter to reposition the existing fragments and to identify the existing active fragments in the FragmentManager cache correctly.
You should not create and add fragments this way. Instead just instantiate the fragments in getItem and the adapter will take care of using them. just do this:
#Override
public Fragment getItem(int position) {
Fragment fragment = new CustomFragment();
fragments.add(fragment)
return fragment
}
I would suggest you don't keep a list of references to fragments since it is not necessary and you risk to create memory leaks.
What i would do is create the fragment only when required like this :
#Override
public Fragment getItem(int position) {
Fragment fragment = new MyFragment();
return fragment;
}
To solve your problem you should create the fragment based on your needs, for example if you have fragments of different class instances like for example one instance of MyFragment another one of YourFragment and so on, just keep a list which says which kind of fragment occupy that position.
For example:
myListMap = new HashMap<Integer, Integer>();
myListMap.put(position, type);
and then create the fragment on the fly:
#Override
public Fragment getItem(int position) {
Fragment fragment = null;
int type = ...find fragment type in that position ....
if(type == MYFRAGMENTTYPE) {
fragment = new MyFragment();
}
return fragment;
}
Don't know if you still need the answer, but I was trying to do something similar and just found the solution :)
You are receiving that exception because of your getItem function. You are returning the fragment in the position that the function receives, and this position is always the last position of the array because that would correspond to the last added fragment in a "typical" usage.
In your case, you want to add a new Fragment in the first position, so your getItem will return twice the same fragment and throw the exception.
To avoid this you need to create a public function into your Adapter and the index of the fragment you are adding, and then return this specific fragment.
PS.: I'm developing only in Kotlin for about 6 months now, so it can have some typos.
public int newFragmentIndex = 0;
public List<Fragment> fragments;
...
public void addFragmentAt(int index, Fragment fragment) {
newFragmentIndex = index;
pageAdapter.fragments.add(index, fragment);
notifyDataSetChanged();
}
public void addFragment(Fragment fragment) {
newFragmentIndex = fragments.size();
pageAdapter.fragments.add(fragment);
notifyDataSetChanged();
}
...
#Override
public Fragment getItem(int position) {
return fragments.get(newFragmentIndex);
}
To call this from your Activity, change your pageAdapter.fragments.add(new CustomFragment());
to
pageAdapter.fragments.addFragment(new CustomFragment());
And
pageAdapter.fragments.add(0, new CustomFragment());
to
pageAdapter.fragments.addFragmentAt(0, new CustomFragment());
Hope this helps you with your problem!

Viewpager with fragments

I have a viewpager in my app with 7 fragments, each represent their own logic
however, there are 2 fragments that are mutually coupled, one cannot exist without the other.
I have 2 buttons, previous and next at the bottom of the screen which switch between the fragments.
I want to make it so the swiping in the viewpager cannot reach one of the coupled fragments
Right now I did this: I put the first fragment (of the coupled ones) to be at location 0, and the 2nd fragment at the last position, so that when you swipe left and right you reach the 2nd coupled fragment last.
I want to make it so you can't reach the last fragment with swiping
how to do that ?
EDIT:
Fragment Adapter
public class PurchaseFragmentStatePagerAdapter extends FragmentStatePagerAdapter {
private List<BaseFragment> fragments;
private boolean shouldShowLastFragment;
#Inject
public PurchaseFragmentStatePagerAdapter(FragmentManager fm) {
super(fm);
}
public void setFragments(List<BaseFragment> fragments) {
this.fragments = fragments;
}
#Override
public BaseFragment getItem(int position) {
return fragments.get(position);
}
#Override
public int getCount() {
return fragments == null ? 0 : shouldShowLastFragment ? fragments.size() : fragments.size()-1;
}
public void enableItem(int i) {
shouldShowLastFragment = true;
notifyDataSetChanged();
}
public void disableItem(int i) {
shouldShowLastFragment = false;
notifyDataSetChanged();
}
}
I assume you have either a FragmentStatePagerAdapter or a FragmentPagerAdapter.
In your adapter, setting the getCount() method to 6, will make sure that you cannot reach the last fragment.
#Override
public int getCount() {
return 6;
}

Fragment constantly updated

I've got a FragmentActivity when I instantiate three different (n, n+1, n+2) Fragments.
I need to keep each Fragment updated when user swipes to it, so I used ViewPager.SimpleOnPageChangeListener.onPageSelected in the Fragment Activity, so when user swipes to n+1 or n+2 Fragment and again to n that function update the content.
Without using this workaround if I'm in the Fragment n+1, both n and n+2 are already loaded! I'd like instead that the Fragment load when the user swipes to it, without "pre-load".
This workaround works fine for me but it has a problem: the n Fragment that is the first in the list at start up of the app doesn't load its content. To load its content I have to swipe to n+1 then go back to n.
I know that the content of the Fragment should be setted on the class called at the moment of instantiate the fragment and that extends Fragment class, but in this way I don't know how to keep up to date each Fragment, as I do using onPageSelected.
Any suggestions?
EDIT 1:
I istantiate my fragments in this way in onCreate():
for(int x = 0; x < 3; x++) {
Bundle b = new Bundle();
b.putString( "id" , x );
Fragment myFrag = Fragment.instantiate( myContext , Mm_FragmentPage.class.getName() );
myFrag.setArguments( b );
fragments.add(myFrag);
}
Then I set the adapter in the ViewPager:
mPagerAdapter = new PagerAdapter( super.getSupportFragmentManager() , fragments );
mPager.setAdapter( mPagerAdapter );
Then I use the adapter in the TitlePageIndicator
titleIndicator = (TitlePageIndicator) findViewById( R.id.titleFragments );
titleIndicator.setViewPager( mPager );
titleIndicator.setOnPageChangeListener( new myPageChangeListener() );
And, at the end, the class PagerAdapter:
public class PagerAdapter extends FragmentPagerAdapter
{
// fragments to instantiate in the viewpager
private List<Fragment> fragments;
// constructor
public PagerAdapter(FragmentManager fm, List<Fragment> fragments)
{
super(fm);
this.fragments = fragments;
}
// return access to fragment from position, required override
#Override
public Fragment getItem(int position)
{
return this.fragments.get(position);
}
// number of fragments in list, required override
#Override
public int getCount()
{
return this.fragments.size();
}
#Override
public CharSequence getPageTitle(int position)
{
return getResources().getStringArray( R.array.tab_header_name )[ position ];
}
}
OK, so first thing you need to set OnPageChangeListener on the ViewPager and implement method onPageSelected(int i) and call the adapter's notifyDataSetChanged(), like so:
mPager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {
#Override
public void onPageScrolled(int i, float v, int i2) {
}
#Override
public void onPageSelected(int i) {
//Tell the adapter that the content was changed
mPager.getAdapter().notifyDataSetChanged();
}
#Override
public void onPageScrollStateChanged(int i) {
}
});
In order to keep the fragments updated, you need to extends FragmentStatePagerAdapter and not FragmentPagerAdapter like what you did. The difference is that with FragmentPagerAdapter the ViewPager will never re-create the fragments, while in FragmentStatePagerAdapter it will.
Then on getItem(..) make sure to return a new instance of the fragment with the new content by passing the content to its arguments via setArguments(). Then override also getItemPosition(..) to tell the adapter that the fragment is not found, and therefore it must re-create it.
public class MyPagerAdapter extends FragmentStatePagerAdapter {
//List to hold the fragments to be shown
//NOTE: It's a list of Fragment classes, not a list of Fragment instances!
private List<Class<? extends Fragment> fragments;
public MyPagerAdapter(FragmentManager fm) {
super(fm);
fragments.add(SomeFragment.class);
fragments.add(AnotherFragment.class);
fragments.add(MoreFragment.class);
}
#Override
public Fragment getItem(int i) {
try {
//Creates a new instance of the fragment
Fragment instance = fragments.get(i).newInstance();
//Put the new content by passing Bundle with new content
instance.setArguments(args);
return instance;
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return null;
}
#Override
public int getItemPosition(Object object) {
//NOTE: you might want to put better logic here
return POSITION_NONE;
}
#Override
public int getCount() {
return pages.size();
}
}
Every time you slide from one fragment to another, onPageSelected() will be fired calling notifyDataSetChanged() which will force the adapter to check also if the position of the fragment has changed. Since we return POSITION_NONE in getItemPosition(..), the adapter thinks that the position changed and will then call getItem(i). In getItem(i) we return a new instance (optionally, passing new arguments). Problem solved! :)
I just tested it by myself, created a small app that have a counter which increases everytime the user slides the page and it works!
This way you can drop the ViewPager.SimpleOnPageChangeListener.onPageSelected.
Learn more about ViewPager.

Categories

Resources