I understand that the offscreen page limit for a viewpager must be at least 1. I am trying to dynamically change the fragments in my viewpagers as I am constantly grabbing information from a server.
For example, I have Fragments A, B, C instantiated when I am viewing Fragment B.
I want to change Fragment A's info, so I update it and call notifyDataSetChanged(). I have not created a new Fragment and inserted it in its place, but changed the imageview associated with the original fragment.
However, once I try to navigate to A, I run into an error saying "Fragment is not currently in the FragmentManager "
Can anyone explain to me how I'd be able to jump back and forth between immediate pages in a viewpager while allowing these pages to change their views?
I didn't do that, but my suggestion will be not to try to. Android does a lot of magic under the hood, and it is very possible you'll face a lot of issues trying to implement what you want. I had an experience with ListView when I was trying to save contentView for each list item, so that Android will render them only once, after the whole day I gave up the idea because every time I've changed the default behavior, something new came up (like exceptions that views are having two parents). I've managed to implement that, but the code was really awful.
Why don't you try, for example, save the image you've downloaded on the disc, and retrieve if when fragment actually appears on the screen ? Picasso library could help in this case
To do that, you should create a FragmentPagerAdapter that saves references to your pages as they are created. Something like this:
public class MyPagerAdapter extends FragmentPagerAdapter {
SparseArray<Fragment> registeredFragments = new SparseArray<Fragment>();
public MyPagerAdapter(FragmentManager fm) {
super(fm);
}
#Override
public Fragment getItem(int position) {
switch (position)
{
case 0:
return MyListFragment.newInstance(TAB_NAME_A);
case 1:
return MyListFragment.newInstance(TAB_NAME_B);
case 2:
return MyListFragment.newInstance(TAB_NAME_C);
default:
return null;
}
}
#Override
public int getCount() {
// Show 3 total pages.
return TOTAL_PAGES;
}
#Override
public Object instantiateItem(ViewGroup container, int position) {
Fragment fragment = (Fragment) super.instantiateItem(container, position);
registeredFragments.put(position, fragment);
return fragment;
}
#Override
public void destroyItem(ViewGroup container, int position, Object object) {
registeredFragments.remove(position);
super.destroyItem(container, position, object);
}
public Fragment getRegisteredFragment(int position) {
return registeredFragments.get(position);
}
}
You can access a particular page like this:
((MyListFragment) myPagerAdapter.getRegisteredFragment(0)).updateUI();
Where updateUI() is a custom method that updates your list on that page and calls notifyDataSetChanged().
Related
When I start the app everything works ok but when I rotate to landscape it crashes because in the Fragment there is a field that is NULL.
I dont use setRetainInstance(true) or adding Fragments to FragmentManagerI create new Fragments on app start and when app rotate.
In the Activity OnCreate() I create the Fragment and adding them to the viewPager like this.
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ParentBasicInfoFragment parentBasicInfoFragment = new ParentBasicInfoFragment();
ParentUTCFragment parentUTCFragment = new ParentUTCFragment();
ParentEventsFragment parentEventsFragment = new ParentEventsFragment();
this.mFragments = new ArrayList<>();
this.mFragments.add(parentBasicInfoFragment);
this.mFragments.add(parentUTCFragment);
this.mFragments.add(parentEventsFragment);
this.viewpage.setOffscreenPageLimit(3);
setCurrentTab(0);
this.viewpage.setAdapter(new MainActivityPagerAdapter(getSupportFragmentManager(), this.mFragments));
}
Then I have a test button on the app that when I press it will do like
public void test(View view) {
((BaseFragment) MainActivity.this.mFragments.get(MainActivity.this.viewpage.
getCurrentItem())).activityNotifiDataChange("hello");
}
This will work and the current Fragments in the ViewPager have the method, activityNotifiDataChange() that are being called and all is ok.
When I rotate the app and do the same thing pressing the button the activityNotifiDataChange() is being called alright but there a null pointer exception because the ArrayList<Fragment> mFragment is now NULL.
Here´s a small sample Android Studio project showing this behavior:
https://drive.google.com/file/d/1Swqu59HZNYFT5hMTqv3eNiT9NmakhNEb/view?usp=sharing
Start app and press button named "PRESS TEST", then rotate device and press the button again and watch the app crash
UPDATE SOLUTION thanks #GregMoens and #EpicPandaForce
public class MainActivityPagerAdapter extends PersistenPagerAdapter<BaseFragment> {
private static int NUM_ITEMS = 3;
public MainActivityPagerAdapter(FragmentManager fm) {
super(fm);
}
public int getCount() {
return NUM_ITEMS;
}
#Override
public Fragment getItem(int position) {
switch (position) {
case 0:
return ParentBasicInfoFragment.newInstance(0, "Page # 1");
case 1:
return ParentUTCFragment.newInstance(1, "Page # 2");
case 2:
return ParentEventsFragment.newInstance(2, "Page # 3");
default:
return null;
}
}
}
public abstract class PersistenPagerAdapter<T extends BaseFragment> extends FragmentPagerAdapter {
private SparseArray<T> registeredFragments = new SparseArray<T>();
public PersistenPagerAdapter(FragmentManager fragmentManager) {
super(fragmentManager);
}
#Override
public T instantiateItem(ViewGroup container, int position) {
T fragment = (T)super.instantiateItem(container, position);
registeredFragments.put(position, fragment);
return fragment;
}
#Override
public void destroyItem(ViewGroup container, int position, Object object) {
registeredFragments.remove(position);
super.destroyItem(container, position, object);
}
public T getRegisteredFragment(ViewGroup container, int position) {
T existingInstance = registeredFragments.get(position);
if (existingInstance != null) {
return existingInstance;
} else {
return instantiateItem(container, position);
}
}
}
The main problem I see with your app is your misunderstanding with how FragmentPagerAdapter works. I see this a lot and it's due to lack of good javadocs on the class. The adapter should be implemented so that getItem(position) returns a new fragment instance when called. And then getItem(position) will only be called by the pager when it needs a new instance for that position. You should not pre-create the fragments and pass then into the adapter. You should also not be holding strong references to the fragments from either your activity or from parent fragments (like ParentBasicInfoFragment). Because remember, the fragment manager is managing fragments and you are also managing fragments by newing them and keeping references to them. This is causing a conflict and after rotation, you are trying to invoke activityNotifiDataChange() on a fragment that is not actually initialized (onCreate() was not called). Using the debugger and tracking object IDs will confirm this.
If you change your code so that the FragmentPagerAdapter creates the fragments when they are needed and don't store references to fragments or lists of fragments, you will see much better results.
this.mFragments = new ArrayList<>();
Because this is wrong. You should never hold a reference to the fragment list if you are using ViewPager. Just return new instance of Fragment from getItem(int position) of FragmentPagerAdapter and you are good to go.
But to fix your code, you must delete mFragments entirely.
See https://stackoverflow.com/a/58605339/2413303 for more details.
Use setRetainInstance(true) is not a good approach. If you need to same some simple information such as: position of recyclerView, selected item of recyclerView, maybe some model(Parcelable) you could do it with method onSaveInstanceState / onRestoreInstanceState, there is one limitation is 1MB. Here is an example with animations how it works.
For more durable persistance use SharedPreferences, Room(Google ORM) or you could try to use ViewModel with LiveData (best approach some data which should live while user session).
//change in AndroidManifest.xml file,
<activity
android:name=".activity.YourActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:screenOrientation="sensor"
/>
//YourActivity in which you defines fragment.
//may be it helps
I have several fragments within a viewpager, and the first fragment (TimelineFragment) is being replaced whenever the user chooses to browse a different community.
I'm able to successfully change the TimelineFragment when I changed my FragmentPagerAdapter to FragmentStatePagerAdapter since FragmentPagerAdapter does not update the fragment even when I use pagerAdapter.notifyDataSetChanged()
Hence, I used FragmentStatePagerAdapter with the following code:
#Override
public Fragment getItem(int position) {
return fragmentList.get(position);
}
public void setItem(int position, Fragment fragment) {
fragmentList.set(position, fragment);
}
#Override
public int getCount() {
return fragmentList.size();
}
#Override
public int getItemPosition(Object object) {
return PagerAdapter.POSITION_NONE;
}
The problem is whenever I move to a fragment, that fragment is being refreshed. Which makes my recyclerview within each fragment reload all lists and go to the top of that list.
My goal here is that when the user has chosen to view a different community, the timeline fragment (ALONE, hopefully exluding other fragments) will be the only fragment to reload, and load the respective list based on the chosen community.
the method onCreateView in fragment calls each time you move between 3 fragments in view pager. so the solution is:
1- add static boolean loaded=false; in your fragment
2- before calling the code that refreshes the recyclerview or any code you don't want to call add
if(!loaded){
//your code
...
loaded=true;
}
I have a FragmentPagerAdapter for a viewPager Which initially has only one Fragment in it. I want to dynamically add a new Fragment to the adapter when user swipes from right to left, and dynamically remove a Fragment when user swipes from left to right.I have tried to use this library https://github.com/commonsguy/cwac-pager but in that library we have an option to add and remove fragments on button clicks. I have tried to add a OnPageChangeListener to the viewpager but the callback methods ( onPageScrolled and onPageScrollStateChanged) are being called more than once which results in addition of more than one fragment to the FragmentPagerAdapter. So please shed some light on how to do this.
#dora: i think in your case FragmentStatePagerAdapter will help you. I have mentioned its use below as per my understanding.I hope it will help you in taking decision.
There are two ways to implement ViewPager:
• FragmentStatePagerAdapter
• FragmentPagerAdapter
FragmentStatePagerAdapter class consumes less memory, because it destroys fragments, as soon as they are not visible to user, keeping only saved state of that fragment
FragmentPagerAdapter: when there are less number of fragments. But using AndroidFragmentPagerAdapter for large number of fragments would result choppy and laggy UX.
Number of page hold by a viewPager?
The number of items that any ViewPager will keep hold of is set by the setOffscreenPageLimit() method. The default value for the offscreen page limit is 3. This means ViewPager will track the currently visible page, one to the left, and one to the right. The number of tracked pages is always centered around the currently visible page.
Please follow this link for code: http://www.truiton.com/2013/05/android-fragmentpageradapter-example/
I know this post is old, but I struggled to figure this out so I'll answer it anyway.
You want to use FragmentStatePagerAdapter and override getItemPosition(). Create a list of stuff you want to pass down to the fragment, call notifyDataSetChanged(), and you're all set!
Here's the adapter:
public class SectionsPagerAdapter extends FragmentStatePagerAdapter {
List<String> mKeyList = new ArrayList<>();
public SectionsPagerAdapter(FragmentManager fm) {
super(fm);
}
#Override
public int getItemPosition(Object object) {
return POSITION_NONE;
}
#Override
public Fragment getItem(int position) {
// getItem is called to instantiate the fragment for the given page.
// Return a PlaceholderFragment (defined as a static inner class below).
return PlaceholderFragment.newInstance(mKeyList.get(position));
}
#Override
public int getCount() {
return mKeyList.size();
}
#Override
public CharSequence getPageTitle(int position) {
return "SCOUT " + (getCount() - position);
}
public void add(int position, String key) {
mKeyList.add(position, key);
notifyDataSetChanged();
}
}
And here's the fragment:
public static class PlaceholderFragment extends Fragment {
private static final String ARG_SCOUT_KEY = "scout_key";
public static PlaceholderFragment newInstance(String key) {
PlaceholderFragment fragment = new PlaceholderFragment();
Bundle args = new Bundle();
args.putString(ARG_SCOUT_KEY, key);
fragment.setArguments(args);
return fragment;
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.current_scout_fragment, container, false);
//getArguments().getString(ARG_SCOUT_KEY));
return rootView;
}
}
I want to dynamically add a new Fragment to the adapter when user swipes from right to left, and remove dynamically remove a Fragment when user swipes from left to right.
AFAIK, that will not be supported by any PagerAdadpter. It certainly will not be supported by ArrayPagerAdapter. The page needs to exist, otherwise you cannot swipe to it. You cannot swipe first, then add the page later.
Moreover, I have never found a use case for your proposed pattern that could not be handled by having the page be in the adapter, but not populating the page (i.e., whatever the expensive work is that you appear to be trying to avoid) until the swipe begins.
I understand the lowest number I can give setOffscreenPageLimit(int) is 1. but I need to load one page at a time because memory problems.
Am i going to have to use the old style tabhost etc? or is there a way/hack I can make my viewPager load one page at a time?
My Adapter extends BaseAdapter with the ViewHolder patern.
I was having the same problem and I found the solution for it:
Steps:
1) First Download the CustomViewPager Class from this link.
2) Use that class as mentioned below:
In Java:
CustomViewPager mViewPager;
mViewPager = (CustomViewPager) findViewById(R.id.swipePager);
mViewPager.setOffscreenPageLimit(0);
In XML:
<com.yourpackagename.CustomViewPager
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/swipePager"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
Now only one page will be loaded at once.
P.S: As per the question's requirement, I have posted the solution for Viewpager. I haven't tried the same with TabLayout yet. If I will find any solution for that I will update the answer.
In this file, KeyEventCompat is used it may not found by the android studio because KeyEnentCompat class was deprecated in API level 26.0.0 so you need to replace KeyEventCompat to event for more details you can view
https://developer.android.com/sdk/support_api_diff/26.0.0-alpha1/changes/android.support.v4.view.KeyEventCompat
As far as I know, that is not possible when using the ViewPager. At least not, when you want your pages to be swipe-able.
The explaination therefore is very simple:
When you swipe between two pages, there is a Point when both pages need to be visible, since you cannot swipe between two things when one of those does not even exist at that point.
See this question for more: ViewPager.setOffscreenPageLimit(0) doesn't work as expected
CommonsWare provided a good explaination in the comments of his answer.
but I need to load one page at a time because memory problems.
That presumes that you are getting OutOfMemoryErrors.
Am i going to have to use the old style tabhost etc?
Yes, or FragmentTabHost, or action bar tabs.
or is there a way/hack I can make my viewPager load one page at a time?
No, for the simple reason that ViewPager needs more than one page at a time for the sliding animation. You can see this by using a ViewPager and swiping.
Or, you can work on fixing your perceived memory problems. Assuming this app is the same one that you reported on earlier today, you are only using 7MB of heap space. That will only result in OutOfMemoryErrors if your remaining heap is highly fragmented. There are strategies for memory management (e.g., inBitmap on BitmapOptions for creating bitmaps from external sources) that help address such fragmentation concerns.
My Adapter extends BaseAdapter with the ViewHolder patern.
BaseAdapter is for use with AdapterView, not ViewPager.
I have an Answer for this. The above said method setUserVisibleHint() is deprecated and you can use setMaxLifecycle() method. For loading only the visible fragment you have to set the behaviour to BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT in the viewpager adapter. ie; in the Constructor. And for handling the fragment use onResume() method in the fragment.
In this way you can load only one fragment at a time in the viewpager.
public static class MyAdapter extends FragmentStatePagerAdapter {
public MyAdapter(FragmentManager fm) {
super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
}
#Override
public int getCount() {
return NUM_ITEMS;
}
#Override
public Fragment getItem(int position) {
return ArrayListFragment.newInstance(position);
}
}
In Kotlin:
class MyAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm,BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT )
Also use with FragmentPagerAdapter (now deprecated) in same way
By using this method you can load one page at time in tab layout with view pager`
#Override
public void onResume() {
super.onResume();
if (getUserVisibleHint() && !isVisible) {
Log.e("~~onResume: ", "::onLatestResume");
//your code
}
isVisible = true;
}
#Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (isVisibleToUser && isVisible) {
Handler handler = new Handler();
handler.postDelayed(new Runnable() {
#Override
public void run() {
//your code
}
}, 500);
}
}
`
Override the setUserVisibleHint and add postDelayed like below in your every fragments.
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
if (isVisibleToUser)
Handler().postDelayed({
if (activity != null) {
// Do you stuff here
}
}, 200)
super.setUserVisibleHint(isVisibleToUser)
}
I can manage by this way and its working fine now for me.
First, copy in the SmartFragmentStatePagerAdapter.java which provides the intelligent caching of registered fragments within our ViewPager. It does so by overriding the instantiateItem() method and caching any created fragments internally. This solves the common problem of needing to access the current item within the ViewPager.
Now, we want to extend from SmartFragmentStatePagerAdapter copied above when declaring our adapter so we can take advantage of the better memory management of the state pager:
public abstract class SmartFragmentStatePagerAdapter extends FragmentStatePagerAdapter {
// Sparse array to keep track of registered fragments in memory
private SparseArray<Fragment> registeredFragments = new SparseArray<Fragment>();
public SmartFragmentStatePagerAdapter(FragmentManager fragmentManager) {
super(fragmentManager);
}
// Register the fragment when the item is instantiated
#Override
public Object instantiateItem(ViewGroup container, int position) {
Fragment fragment = (Fragment) super.instantiateItem(container, position);
registeredFragments.put(position, fragment);
return fragment;
}
// Unregister when the item is inactive
#Override
public void destroyItem(ViewGroup container, int position, Object object) {
registeredFragments.remove(position);
super.destroyItem(container, position, object);
}
// Returns the fragment for the position (if instantiated)
public Fragment getRegisteredFragment(int position) {
return registeredFragments.get(position);
}
}
// Extend from SmartFragmentStatePagerAdapter now instead for more dynamic ViewPager items
public static class MyPagerAdapter extends SmartFragmentStatePagerAdapter {
private static int NUM_ITEMS = 3;
public MyPagerAdapter(FragmentManager fragmentManager) {
super(fragmentManager);
}
// Returns total number of pages
#Override
public int getCount() {
return NUM_ITEMS;
}
// Returns the fragment to display for that page
#Override
public Fragment getItem(int position) {
switch (position) {
case 0: // Fragment # 0 - This will show FirstFragment
return FirstFragment.newInstance(0, "Page # 1");
case 1: // Fragment # 0 - This will show FirstFragment different title
return FirstFragment.newInstance(1, "Page # 2");
case 2: // Fragment # 1 - This will show SecondFragment
return SecondFragment.newInstance(2, "Page # 3");
default:
return null;
}
}
// Returns the page title for the top indicator
#Override
public CharSequence getPageTitle(int position) {
return "Page " + position;
}
}
You actually don't need a custom ViewPager.
I had the same issue and I did like this.
Keep the setOffscreenPageLimit() as 1.
Use fragment's onResume and onPause lifecycle methods.
Initialize and free-up memories on these lifecycle methods.
I know this is an old post, but I stumbled upon this issue and found a good fix if your loading fragments. Simply, check if the user is seeing the fragment or not by overriding the setUserVisibleHint(). After that load the data.
#Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (isVisibleToUser) {
getData(1, getBaseUrl(), getLink());
}
}
OK. I'm quite new to Android, and I have a problem with ViewPager and FragmentStatePagerAdapter. I have a chat application in which the chats are shown in the ViewPager. The chats can be closed dynamically and this works perfectly until I navigate out of my app, and return back to the chat activity (and onCreate of the activity is called again).
Let's say I have one chat window open. When I leave the activity, return later, and try to close the page, I get this exception:
java.lang.IllegalStateException: The application's PagerAdapter changed the adapter's contents without calling PagerAdapter#notifyDataSetChanged! Expected adapter item count: 1, found: 0
Here's my FragmentStatePagerAdapter:
private class ChatPageAdapter extends FragmentStatePagerAdapter {
private Map<Integer, Fragment> pageReferenceMap = new HashMap<Integer, Fragment>();
private FragmentManager fragmentManager;
public ChatPageAdapter(FragmentManager fm) {
super(fm);
this.fragmentManager = fm;
}
public ChatFragment getItem(int position) {
Fragment fragment = ChatFragment.newInstance(Chat.getChat(position).getUniqueID());
pageReferenceMap.put(position, fragment);
return (ChatFragment) fragment;
}
#Override
public int getItemPosition(Object object) {
return POSITION_NONE;
}
public int getCount() {
return Chat.getChatCount();
}
public Object instantiateItem(ViewGroup container, int position) {
return super.instantiateItem(container, position);
}
public void destroyItem(ViewGroup container, int position, Object object) {
super.destroyItem(container, position, object);
pageReferenceMap.remove(position);
}
public ChatFragment getFragment(int position) {
return (ChatFragment) pageReferenceMap.get(position);
}
}
The chats are stored statically in the Chat-class, and getChatCount() simply returns the size of the ArrayList. I am not modifying the ArrayList in anywhere but two places, and these two places should be in sync with notifyDataSetChanged()... that is until the activity is recreated.
In my open(Chat) method I check if the Chat has already been "registered", and trigger notifyDataSetChanged only if it has not (to improve performance).
In my close(Chat) method I simply remove the Chat from the Chat-class, call notifyDataSetChanged on the adapter, and BOOM, the exception is thrown.
Is there somekind of culprit I should be aware of when the PageViewer and it's adapter is recreated? How do I keep the new adapter in sync with the already opened chats?
I've tried lots of things but nothing seems to work... I'm sure there's a simple solution to this problem.
Thanks a lot.
After ADT 22 the PagerAdapter has gotten very strict about calling notifyDataSetChanged() before calling getCount().
So the solution is simply to call notifyDataSetChanged() on the adapter every time the size of the data changes.
OK... I got this one eventually figured out. It was a stupid listener problem on my behalf. I forgot to unregister a listener from a chat when the activity was closed, and my onClosed(Chat) method got called twice.