Changing page count in FragmentStatePagerAdapter's getCount() based on preferences - android

After reading up hordes of SO answers, this challenge remains.
This is the relevant piece of code in a v4 FragmentStatePagerAdapter:
#Override
public int getCount()
{
int count = 2;
if (pref_dealsOnly)
count = 1;
return count;
}
Like you already guessed, it just crashes with the following exception:
java.lang.IllegalStateException: The application's PagerAdapter changed the adapter's contents without calling PagerAdapter#notifyDataSetChanged! Expected adapter item count: 2, found: 1 Pager id: com.droid.shopper:id/shopperMainPager Pager class: class android.support.v4.view.ViewPager Problematic adapter: class com.sndroid.globeshopper.shopper.ShopperJournal$JournalPagerAdapter
I fail to understand why the exception occurs.
Please help on how to accomplish what seems to be so simple and apparent - to return a getCount() value in a v4 FragmentStatePagerAdapter dynamically. Kindly let me know if more code is required.
Thanks in advance!

getCount() may be called several times by the ViewPager. It must remain constant, for the life of that PagerAdapter, for reliable results with FragmentPagerAdapter and FragmentStatePagerAdapter.
In theory, you could do what the error message tells you and call notifyDataSetChanged() on the PagerAdapter at the point when getCount() will start returning a different value. While that can work with a custom PagerAdapter implementation, neither FragmentPagerAdapter nor FragmentStatePagerAdapter used to handle it all that well. I wound up creating another PagerAdapter implementation to support adding and removing pages. It is possible that FragmentPagerAdapter and/or FragmentStatePagerAdapter are behaving better nowadays, though I doubt it.
In your case, it may be a matter of swapping in a different PagerAdapter at the point when pref_dealsOnly changes value.

Related

ViewPager2 default position

I'm creating a slideshow with ViewPager2. For example, the slideshow has 3 items and I want to show the second item when the activity opens. I use setCurrentItem(int item, boolean smoothScroll) method but it doesn't work and nothing happens. How can I achieve it?
viewPager.adapter = adapter
viewPager.setCurrentItem(1, true)
I think an easier more reliable fix is to defer to next run cycle instead of unsecure delay e.g
viewPager.post {
viewPager.setCurrentItem(1, true)
}
setCurrentItem(int item, boolean smoothScroll) works correctly in ViewPager but in ViewPager2 it does not work as expected. Finally, I faced this problem by adding setCurrentItem(int item, boolean smoothScroll) method into a delay like this:
Handler().postDelayed({
view.viewPager.setCurrentItem(startPosition, false)
}, 100)
Do not use timers, you will run into a lot of probable states in which the user has a slow phone and it actually takes a lot longer than 100 ms to run, also, you wouldn't want too slow of a timer making it ridiculously un-reliable.
Below we do the following, we set a listener to our ViewTreeObserver and wait until a set number of children have been laid out in our ViewPager2's RecyclerView (it's inner working). Once we are sure x number of items have been laid out, we start our no-animation scroll to start at the position.
val recyclerView = (Your ViewPager2).getChildAt(0)
recyclerView.apply {
val itemCount = adapter?.itemCount ?: 0
if(itemCount >= #(Position you want to scroll to)) {
viewTreeObserver.addOnGlobalLayoutListener(object: ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
viewTreeObserver.removeOnGlobalLayoutListener(this)
// False for without animation scroll
(Your ViewPager2).scrollToPosition(#PositionToStartAt, false)
}
}
}
First off, I think that the accepted answer shouldn't be #hosseinAmini 's, since it's suggesting to use a delay to work around the problem. You should first be looking for what the assumed bug is caused by, rather than trusting unreasonable solutions like that.
#Rune's proposal is correct, instead; so I'm quoting their code in my answer:
viewPager.post {
viewPager.setCurrentItem(1, true)
}
The only thing I'd argue about is the aforementioned one's belief that their solution is just deferring the execution of that lambda in the next run cycle. This wouldn't make anything buggy work properly. Rather, what it is actually being done is deferring the execution of that lambda to once the view has been attached to a window, which implies it's also been added to a parent view. Indeed, there looks to be an issue as to changing the current ViewPager2 item before being attached to a window. Some evidence to support this claim follows:
Using whichever Handler won't work nearly as effectively.
Handler(Looper.getMainLooper()).post {
viewPager.setCurrentItem(1, true) // Not working properly
}
From a theoretical standpoint, it might incidentally work due to the ViewPager2 being attached to a window acquiring priority in the message queue of the main looper, but this shouldn't ever be relied upon as there's just no guarantee that it'll work (it's even more likely it won't) and if it even turned out to be working, further investigation running multiple tests should make my point clear.
View.handler gets null, which means the view hasn't been attached to any window yet.
View.handler // = null
Despite Android UI being tied to the main looper, which will always uniquely correspond to the main thread –hence also called the UI thread,– a weird design choice stands in the handler not being associated to each view until they get attached to a window. A reason why this may lay on the consequent inability of views to schedule any work on the main thread while they're not part of the hierarchy, which may turn useful when implementing a view controller that schedules view updates while unaware of their lifecycle (in which case it would employ the View's handler, if any — or just skip scheduling whatever it was going to if none).
EDIT:
Also, #josias has pointed out in a comment that it'd be clearer to use:
viewPager.doOnAttach {
viewPager.setCurrentItem(1, true)
}
Thanks for that suggestion! It expresses better the actual intent, rather than relying on the behavior of the View.post method.
Do not use timers and all that stuff with 'post', it's not the reliable solution and just a piece of code that smells.
Instead, try use viewPager.setCurrentItem(1, false). That 'false' is about smoothScroll, you can't smooth scroll your viewPager2 when your activity is just opened. Tested it on a fragment in onViewCreated() method, it also didn't work with "true", but works with "false"
As it was mentioned above you have to use setCurrentItem(position, smoothScroll) method on ViewPager2 in order to show selected item. To make it work you have to define a callback, here is an example:
ViewPager2.OnPageChangeCallback callback = new ViewPager2.OnPageChangeCallback() {
#Override
public void onPageSelected(int position) {
super.onPageSelected(position);
}
};
And then you have to register it as follow:
viewPager.registerOnPageChangeCallback(callback);
Also do not forget to unregister it:
viewPager.unregisterOnPageChangeCallback(callback);
When you call setCurrentItem(position) method it will call onPageSelected(int position) method from your callback passing your argument, and then method createFragment(int position) from FragmentStateAdapter class will be called to show your fragment.
I tried changing viewpager2 page in Handler().dely() and viewPager2.post{} and even 'viewPager2.get(0).post all didn't work for me, I'm using ViewPager with FragmentStateAdapter with Tablayout.
What worked for me is changing the position of the RecylerView in ViewPager2 after binding FragmentStateAdapter to yourViewPager2View.adapter manually:
(yourViewPager2View[0] as RecyclerView).scrollToPosition(moveToTabNumber)
Why
My problem is onCreateFragment(position:Int):Fragmeet function in FragmentStateAdapter always starting fragment at 0 position no matter what pageNumber I set the page
viewPager.setCurrentItem = pageNumber
I checked where it's called in FragmentStateAdapter it's called in FragmentStateAdapter:
onBindViewHolder(final #NonNull FragmentViewHolder holder, int position)`
so all I needed is to force onBindViewHolder to call onCreateFragment(position:Int) with the page number I wanted.
mViewPager.setCurrentItem(1, true); ---> this is sufficient as you written above
That should work,
in doubt, just check your position:
#Override
public void onPageSelected(int i) {
if (LOG_DEBUG) Log.v(TAG, " ++++++++ onPageSelected: " + i);
mViewPager.setCurrentItem(i);
//TODO You can use this position: to write other dependent logic
}
and also check
getItem(int position) in PagerAdapter
or else paste your code.
I noticed that it works fine when the view is initially created if you opt to not animate it.
viewPager2.setCurrentItem(index, false)
This is usually fine depending on your use case - this initial/default item probably doesn't need to be animated in.
I met the same problem. In my case, I make the viewPager2 Gone by default until network requests succeed, I fix it by setting the CurrentItem after I make the viewPager2 visible.
My answer may not be helpful now but i see no harm to post my expreince, i just came to this problem using ViewPager and ViewPager2 and unexpectedly solved it by just changing some line codes order.
Here is (java) solution for ViewPager:
reviewWordViewPager.addOnPageChangeListener(changeListener);
reviewWordViewPager.setCurrentItem(viewPosition, true/false);
reviewWordTabIndicator.setupWithViewPager(reviewWordViewPager, true);
(Java) solution for ViewPager2:
wordViewPager.registerOnPageChangeCallback(viewPager2OnPageChangeCallback);
wordViewPager.setCurrentItem(vpPosition, true/false);
new TabLayoutMediator(tabIndicator, wordViewPager,
((tab, position) -> tab.setText(viewPagerTitle[position]))).attach();
I did not look up for ViewPager2 whether it needs the following old code used in ViewPager
#Override
public int getItemPosition(#NonNull Object object) {
// refresh all fragments when data set changed
return POSITION_NONE;
}
But surprisingly no need for it in ViewPager2 to solve the problem i've been having, hope it helps others
In case you use context.startActivity to start new activities no need to use wordViewPager.setCurrentItem(item, smoothScroll) in your onResume function to get back to the last selected tab before you started new activity you just save ViewPager/ViewPager2 position like vpPisition = wordViewPager.getCurrentItem(); in onStop function.
vpPisition is a global variable.
as #Daniel Kim but a java version
RecyclerView rvOfViewPager2 = (RecyclerView) viewPager2.getChildAt(0);
rvOfViewPager2.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener()
{
#Override
public void onGlobalLayout()
{
rvOfViewPager2.getViewTreeObserver().removeOnGlobalLayoutListener(this);
viewPager2.setCurrentItem(currentTabId, false);
}
});
First You need to Initilaze the Main activity under any listener or button You want then After that You need to put this Line..
here MainActvity is the Viewpager Main Class You are using and and 2 is the position where you want to move
MainActivity main = (MainActivity ) mContext;
main.selectTab(2, true);

How many pages are initialled for the first time in PagerAdapter?

I have created pages in Android by using PagerAdapter. When I tried to get value in the third page from EditText, I got NullPointerException.
In the third fragment page, I have initialled that EditText already.
iName = (EditText) view.findViewById(R.id.name);
So does this problem occur while the third page haven´t initialled?
A ViewPager, by default, keeps one fragment on each side in memory. You can change this by setting the offscreen page limit.
From the documentation:
Set the number of pages that should be retained to either side of the
current page in the view hierarchy in an idle state. Pages beyond this
limit will be recreated from the adapter when needed.
This is offered as an optimization. If you know in advance the number
of pages you will need to support or have lazy-loading mechanisms in
place on your pages, tweaking this setting can have benefits in
perceived smoothness of paging animations and interaction. If you have
a small number of pages (3-4) that you can keep active all at once,
less time will be spent in layout for newly created view subtrees as
the user pages back and forth.
You should keep this limit low, especially if your pages have complex
layouts. This setting defaults to 1.
Parameters limit How many pages will be kept offscreen in an idle
state.
Example usage: yourViewPager.setOffscreenPageLimit(2);
When you use the default implementation of setOffscreenPageLimit() it is only loading the one fragment which is to the right of it. For eg. when you are on index 1, it has index 2 loaded in memory but not index 0, so swiping left will have to generate a new fragment from scratch. To test this theory you might want to use setOffscreenPageLimit(2) and then try swiping index 1->0. This in no way is the optimal solution but will help clear out your doubts about the concept.
you can store the fragment state of all the pages in the pages adaper like this and get reference of it.
PagerAdapter
private class ScreenSlidePagerAdapter extends FragmentStatePagerAdapter {
private List<Fragment> fragmentsList;
public ScreenSlidePagerAdapter(FragmentManager fm) {
super(fm);
}
#Override
public Fragment getItem(int position) {
Fragment fragment=new ScreenSlidePageFragment();
fragmentsList.add(fragment, position);
return fragment;
}
#Override
public int getCount() {
return NUM_PAGES;
}
public static List<Fragment> getFragmentsList(){
return fragmentsList;
}
}
In Activity:
List<Fragment> fragList=adapter.getFragmentsList()
View view = fragList.get(position).getView();
if (view !=null) {
view.findViewById(R.id.text_view).setText("Child Accessed :D");
}

setPageTransformer() doesn't work with PagerAdapter

I want to customize animation of ViewPager. So I implements ViewPager.PageTransformer and call
setPageTransformer().
It worked well with FragmentStatePagerAdapter or FragmentPagerAdapter. But not PagerAdapter.
When I call setPageTransformer() (even though PageTransformer does nothing), the page in PagerAdapter display once and automatically dismiss (never display again) except the last one.
When I remove setPageTransformer(), it works normally.
Can anyone know why? Thanks in advance.
You can use the sample code in this link. I use PagerAdapter instead of FragmentPagerAdapter.
EDIT Here is the code I changed.
I think it doesn't work, because the sample code that you posted use Fragments.
In the API of PagerAdapter you can read:
Base class providing the adapter to populate pages inside of a ViewPager. You will most likely want to use a more specific implementation of this, such as FragmentPagerAdapter orFragmentStatePagerAdapter.
When you implement a PagerAdapter, you must override the following methods at minimum:
instantiateItem(ViewGroup, int)
destroyItem(ViewGroup, int, Object)
getCount()
isViewFromObject(View, Object)
So you can't just take the example and change FragmentPagerAdapter to PagerAdpapter.
UPDATE 2014-01-14
I import your code into an existing project where I show a ViewPager.
I think I found the mistake!
Your Adapter works fine (also the PageTransformater).
Please change your method instantiateItem in PagerAdper to this and tell me if this is working for you:
#Override
public Object instantiateItem(ViewGroup container, int position) {
// TODO Auto-generated method stub
View view = LayoutInflater.from(mContext).inflate(R.layout.program_item, null);
/**
// This line cause the strange behaviour
view.setLayoutParams(mParams);
**/
((ImageView) view.findViewById(R.id.img)).setImageResource(R.drawable.default_program);
container.addView(view);
return view;
}

ViewPager switch FragmentPagerAdapter

I have got a ViewPager which is populated by a FragmentPagerAdapter. I want to change from the first adapter two another. The problem is that all pages which were loaded before (while having the first adapter) are still the old ones.
I looked at the source code of FragmentPagerAdapter and guess the problem occurs because of the instatiateItem() implementation. Using the tag, which has position and conatiner id in it, the method checks if there is already a Fragment at the position. When there is a Fragment with this tag it is attached. The container id and position do not change when setting a new adapter so it finds the old Fragments.
Do you know a way to remove all old fragments?
I realized that the Fragment tag uses an id, obtained by getItemId() and not the position. To solve my issue I have overwritten getItemId() and use totally different ids in the different FragmentPagerAdapters.
#Override
public long getItemId(int position) {
return mPagerId*100+position;
}
mPagerId is an unique integer that is assigned in the constructor. If you have more than 100 pages you should replace 100 with 1000.

FragmentPagerAdapter with ViewPager and two Fragments. Go to the first from the second and update first's text

I'm not familiar with FragmentPagerAdapter, so this is going to be one of those questions that we (you) read the description critically.
Structure: I have a FragmentPagerAdapter (code below), that will hold two fragments at a time. The first displays book excerpts, and the second a list of book titles.
Goal: I want to achieve what is described in the title: the user can navigate to the second fragment in the pager, click on a title, and then I want to move the user back to the first fragment and tell the first fragment to update the text. The first fragment has a triggerRefresh method for that.
Code: I believe my problem happens because of the way FragmentPagerAdapter reuses/creates the Fragments (which I don't understand). This is my class:
static class MyFragmentPagerAdapter extends FragmentPagerAdapter {
public MyFragmentPagerAdapter(FragmentManager fm) {
super(fm);
}
#Override
public int getCount() {
return NUM_ITEMS;
}
#Override
public Fragment getItem(int position) {
switch(position) {
case 0:
return new ExcerptsFragment();
case 1:
return new BookListFragment();
default:
throw new IllegalArgumentException("not this many fragments: " + position);
}
}
}
This is how I created the relevant members:
ViewPager mViewPager = (ViewPager) findViewById(R.id.pager);
MyFragmentPagerAdapter mFragmentPagerAdapter = new MyFragmentPagerAdapter(getSupportFragmentManager());
mViewPager.setAdapter(mFragmentPagerAdapter);
And this is what I've tried elsewhere in my Activity, when I receive the callback from the book titles Fragment with the title selected:
mViewPager.setCurrentItem(0); // back to excerpts screen page. It's OK.
// Here's the problem! How to identify the fragment 0
// to ExcerptsFragment and call its triggerRefresh()?!?
Series of problems:
Calling the adapter's getView() won't work because it will return a new instance of ExcerptsFragment, which is not the one currently attached (as expected, throws the exception).
I've seen many people here (example) just storing fragments in the getView(). Is that right? Because by looking at the official examples, seems like an anti-pattern to me (defeat the automatic reference by holding the items). And that is also the opinion here and here (and looks right to me).
Any suggestions? I wouldn't be surprised if I'm not understanding all of this one bit...
Disclaimer: Although this had worked perfectly fine for me before, you should be aware of the classic pitfalls of depending on internal, private behavior. While I wrote tests that would eventually warn me if the internal implementation changed, I have since moved on to greener pastures. And you should, too. As such, the value of this question and its answer is only historical, in my opinion.
Sorry about that question, I think it was the hour.
To solve that problem, I implemented this solution as is. Seems to work just fine. So, I believe it was just a matter of finding the (currently attached) fragment instance by figuring out how its Id is named. The link above explains how it's made.
I opted to answer my own question instead of deleting it because I believe novices like me on these pagers will benefit from a "real case scenario". Most of the answers I've seen talk most about the theory, which is the right way BTW... but without a real example to work on sometimes people like me get lost.
Anyway, here is the last piece of code that I needed (the commented part above):
int n = 0;
mViewPager.setCurrentItem(n); // in the question I had stopped here.
ExcerptsFragment f = (ExcerptsFragment) ContainerActivity.this
.getSupportFragmentManager().findFragmentByTag(getFragmentTag(n));
f.triggerRefresh();
// ... below the helper method: used the solution from the link.
private String getFragmentTag(int pos){
return "android:switcher:"+R.id.pager+":"+pos;
}
So, I'm having a feeling that this is a robust solution, because I'm not holding any references to fragments (thus risking the references being outdated). I kept my own code at a minimum, therefore minimizing the chances of me doing something stupid.
Of course, if you have something to add, to show us, to tell what is wrong in doing it or what can be improved, I'll be glad to hear from you.
I searched for a solution to this problem a while myself. Your approach in principle works, but it will break your code if ever the code of the fragment tag creation in the Android base class implementation changes. This is a quite nasty dependency!
A more elegant approach would be to turn the problem around and keep an instance of your base activity in your fragment. Implement a setter for the tag in your activity and call that inside the fragment upon creation - the tag there is simply available with getTag().
An example implementation can be found here.
I solved this problem by using WeakReferences to the fragments upon creation. See : https://stackoverflow.com/a/23843743/734151
If you find anything wrong with this approach, please comment.

Categories

Resources