47

I've got this strange issue, ViewPager's setCurrentItem(position, false) works perfectly fine, then im switching to another activity, and after I'm back to the first activity, the ViewPager always ends up on the first item. Even though I've added setCurrentItem to onResume method it still ignores it. It's not even throwing any exception when I'm trying to set item to out of bounds index. Though later on when I call this method, when the button "next" is tapped, it works like expected. Checked my code 10 times for any possible calls to setCurrentItem(0) or something but it's just not there at all.

ricardopereira
  • 11,118
  • 5
  • 63
  • 81
Maciej Boguta
  • 1,354
  • 1
  • 12
  • 15

18 Answers18

95

i can't really answer WHY exactly this happens, but if you delay the setCurrentItem call for a few milliseconds it should work. My guess is that because during onResume there hasn't been a rendering pass yet, and the ViewPager needs one or something like that.

private ViewPager viewPager;

@Override
public void onResume() {
    final int pos = 3;
    viewPager.postDelayed(new Runnable() {

        @Override
        public void run() {
            viewPager.setCurrentItem(pos);
        }
    }, 100);
}

UPDATE: story time

so today i had the problem that the viewpager ignored my setCurrentItem action, and i searched stackoverflow for a solution. i found someone with the same problem and a fix; i implemented the fix and it didn't work. whoa! back to stackoverflow to downvote that faux-fix-provider, and ...

it was me. i implemented my own faulty non-fix, which i came up with the first time i stumbled over the problem (and which was later forgotten). i'll now have to downvote myself for providing bad information.


the reason my initial "fix" worked was not because of of a "rendering pass"; the problem was that the pager's content was controlled by a spinner. both the spinners and the pagers state were restored onResume, and because of this the spinners onItemSelected listener was called during the next event propagation cycle, which did repopulate the viewpager - this time using a different default value.
removing and resetting the listener during the initial state restoration fixed the issue.

the fix above kind-of worked the first time, because it set the pagers current position after the onItemSelected event fired. later, it ceased to work for some reason (probably the app became too slow - in my implementation i didn't use 100ms, but 10ms). i then removed the postDelayed in a cleanup cycle, because it didn't change the already faulty behaviour.

update 2: i can't downvote my own post. i assume, honorable seppuku is the only option left.

stefs
  • 18,341
  • 6
  • 40
  • 47
  • What do you mean the pager's content was controlled by a spinner? Are you talking about the ViewPager's underlying implementation, or saying the content of each page in the viewpager contained a spinner? And I'm still having this bug even if the first thing I do in onResume() is call super.onResume(). By doing that, view states should be restored, and I shouldn't have to deal with what you described above – Andrew Orobator Jul 29 '14 at 00:38
  • 1
    no, i mean: depending on the selected item in the spinner, the viewpager (i.e. the adapter) was populated with different items. in onResume both the spinner and the pager were re-initialized and the spinner's onItemSelected callback for updating the viewpager "overwrote" the correctly set viewpager position. it was my own fault, the viewpager worked correctly. – stefs Jul 29 '14 at 11:02
  • I see. I had the same issue but quite a different root cause. My ViewPager was hosted by a fragment and I was using getFragmentManager() instead of getChildFragmentManager() when passing a FragmentManager to my FragmetStatePagerAdapter – Andrew Orobator Jul 29 '14 at 15:25
  • Applying rendering delay. Felt awkward when read your answer but it did solve my problem... – Crawler Feb 11 '15 at 08:34
  • @Crawler: it's a trap! don't do it. it might seem to work at first, and gets unpredictable results a couple of months later when the code had time to stew a bit; then it turns into debugging hell and unpredictable 1-in-a-100 odd-behaviours that leads to testers writing bug reports like "X fails sometimes for no reason". – stefs Feb 11 '15 at 11:45
  • @stefs thanks for the warning.....But I now again stuck in problem.... Do you have any lead in solvin this issue? – Crawler Feb 11 '15 at 12:04
  • 1
    as i said, for me the problem was something entirely different, i.e. calling `setCurrentItem()` twice. i guess the same thing applies in your case too. – stefs Feb 11 '15 at 12:07
  • @stefs that solvs the problem but it is not bringing good experience for the user at all with animation. There must be some better way to do that! – codebased Jun 26 '15 at 10:29
  • @stefs 2:31am in the morning and finally your answer solved my problem by post delaying before setting the viewpager item position as well. – user3560827 Aug 03 '16 at 00:34
  • 1
    @user3560827 i'm sure you're either calling setCurrentItem again afterwards or assign a new adapter, thanks to some lifecycle shenanigans or maybe after some async operation. DO NOT depend on postDelayed, it's a hack and practically technical debt. your problem lies elsewhere! – stefs Aug 17 '16 at 06:33
  • @stefs yes correct Calling setCurrentItem() twice doesn't work..good catch – Girish May 13 '19 at 08:18
  • This is happend in ViewPager2 also;;;;;;;;;;;; wtf – MJ Studio Sep 19 '19 at 02:07
  • This isn't working for me, and neither are any of the other solutions mentioned on this thread. I'm stumped, what a strange problem. – Jonathan Oct 30 '19 at 18:03
  • Thank you @stefs you are saved my day, this really worked for me when I was switching fragments and trying to set the position in onCreate. I guess it needs time to set all fragments etc – Rohan Arora Feb 23 '21 at 07:35
  • 3
    wow, that story deserves a netflix adaptation – Ilya E Sep 26 '21 at 06:41
34

I had a similar issue in the OnCreate of my Activity. The adapter was set up with the correct count and I applied setCurrentItem after setting the adapter to the ViewPager however is would return index out of bounds. I think the ViewPager had not loaded all my Fragments at the point i set the current item. By posting a runnable on the ViewPager i was able to work around this. Here is an example with a little bit of context.

    // Locate the viewpager in activity_main.xml
    final ViewPager viewPager = (ViewPager) findViewById(R.id.pager);

    // Set the ViewPagerAdapter into ViewPager
    viewPager.setAdapter(new ViewPagerAdapter(getSupportFragmentManager()));

    viewPager.setOffscreenPageLimit(2);

    viewPager.post(new Runnable() {
        @Override
        public void run() {
            viewPager.setCurrentItem(ViewPagerAdapter.CENTER_PAGE);
        }
    });
Martin Price
  • 629
  • 7
  • 7
  • @TylerPfaff this just worked for me too. Apparently, the ViewPager ignores "early-into-the-activity" instructions because it's not yet aware of what to render – Chisko Jul 06 '17 at 17:30
  • 1
    @TylerPfaff I believe it has something to do with when the ViewPager and its children are measured. It appears to ignore setCurrentItem until the has completed. The runnable posted on the ViewPager is guaranteed to execute after that measure. – Martin Price Aug 24 '17 at 09:24
17

I found a very simple workaround for this:

    if (mViewPager.getAdapter() != null)
        mViewPager.setAdapter(null);
    mViewPager.setAdapter(mPagerAdapter);
    mViewPager.setCurrentItem(desiredPos);

And, if that doesn't work, you can put it in a handler, but there's no need for a timed delay:

        new Handler().post(new Runnable() {
            @Override
            public void run() {
                mViewPager.setCurrentItem(desiredPos);
            }
        });
android developer
  • 114,585
  • 152
  • 739
  • 1,270
17

ViewTreeObserver can be used to avoid a static delay.

Kotlin:

Feel free to use Kotlin extension as a concise option.

view_pager.doOnPreDraw {
    view_pager.currentItem = 1
}

Please, make sure you have a gradle dependency: implementation 'androidx.core:core-ktx:1.3.2' or above

Java

OneShotPreDrawListener.add(view_pager, () -> view_pager.currentItem = 1);
sanya5791
  • 433
  • 4
  • 5
6

A modern approach in a Fragment or Activity is to call ViewPager.setcurrentItem(Int) function in a coroutine in the context of Dispatchers.Main :

lifecycleScope.launch(Dispatchers.Main) {
   val index = 1
   viewPager.setCurrentItem(index)
}
Marco RS
  • 8,145
  • 3
  • 37
  • 45
  • does not work for me. Its appear that the fragment is not ready weat when set curretn item was called java.lang.IllegalArgumentException: No view found for id 0x7f09021d – Gean Carlos Brandão Jan 18 '23 at 22:00
5

I had similar bug in the code, the problem was that I was setting the position before changing the data.

The solution was simply to set the position afterwards and notify the data changed

notifyDataSetChanged()
setCurrentItem()
petrumo
  • 1,116
  • 9
  • 18
4

I have the same problem and I edit

@Override
public int getCount() { return NUM_PAGES; }

I set NUM_PAGES be mistake to 1 only.

Abdulaziz Noor
  • 6,413
  • 2
  • 19
  • 21
4

I've used the post() method described here and sure enough it was working great under some scenarios but because my data comes from the server, it was not the holy grail.

My problem was that i want to have

notifyDataSetChanged

called at an arbitrary time and then switch tabs on my viewPager. So right after the notify call i have this

ViewUtilities.waitForLayout(myViewPager, new Runnable() {
    @Override
    public void run() {
        myViewPager.setCurrentItem(tabIndex , false);
    }
});

and

public final class ViewUtilities {
    public static void waitForLayout(final View view, final Runnable runnable) {
        view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
            //noinspection deprecation
                view.getViewTreeObserver().removeGlobalOnLayoutListener(this);

                runnable.run();
            }
        });
    }
}

Fun fact: the //noinspection deprecation at the end is because there is a spelling mistake in the API that was fixed after API 16, so that should read

removeOnGlobalLayoutListener
       ^^
      ON Global

instead of

removeGlobalOnLayoutListener
            ^^
            ON Layout

This seems to be covering all cases for me.

Boken
  • 4,825
  • 10
  • 32
  • 42
4

some guy wrote on forums here. https://code.i-harness.com/en/q/126bff9 worked for me

 if (mViewPager.getAdapter() != null)
    mViewPager.setAdapter(null);
mViewPager.setAdapter(mPagerAdapter);
mViewPager.setCurrentItem(desiredPos);
  • 1
    You are the best. This worked at last. i was getting data from a volley json request. the views were initially empty. I tried a lot of solutions but none of them worked. but your worked like magic. thanks. – MindRoasterMir Apr 13 '19 at 11:11
4

Solution (in Kotlin with ViewModel etc.) for those trying to set the current item in the onCreate of Activity without the hacky Runnable "solutions":

class MyActivity : AppCompatActivity() {
    lateinit var mAdapter: MyAdapter
    lateinit var mPager: ViewPager
    // ...

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.fragment_pager)
        // ...
        mainViewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
        mAdapter = MyAdapter(supportFragmentManager)
        mPager = findViewById(R.id.pager)

        mainViewModel.someData.observe(this, Observer { items ->
            items?.let {
                // first give the data to the adapter
                // this is where the notifyDataSetChanged() happens
                mAdapter.setItems(it) 
                mPager.adapter = mAdapter // assign adapter to pager
                mPager.currentItem = idx // finally set the current page
            }
        })

This will obviously do the correct order of operations without any hacks with Runnable or delays.

For the completeness, you usually implement the setItems() of the adapter (in this case FragmentStatePagerAdapter) like this:

internal fun setItems(items: List<Item>) {
    this.items = items
    notifyDataSetChanged()
}
dsalaj
  • 2,857
  • 4
  • 34
  • 43
2

This is a lifecycle issue, as pointed out by several posters here. However, I find the solutions with posting a Runnable to be unpredictable and probably error prone. It seems like a way to ignore the problem by posting it into the future.

I am not saying that this is the best solution, but it definitely works without using Runnable. I keep a separate integer inside the Fragment that has the ViewPager. This integer will hold the page we want to set as the current page when onResume is called next. The integer's value can be set at any point and can thus be set before a FragmentTransaction or when resuming an activity. Also note that all the members are set up in onResume(), not in onCreateView().

public class MyFragment extends Fragment
{
    private ViewPager           mViewPager;
    private MyPagerAdapter      mAdapter;
    private TabLayout           mTabLayout;
    private int                 mCurrentItem = 0; // Used to keep the page we want to set in onResume().

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
    {
        View view = inflater.inflate(R.layout.my_layout, container, false);
        mViewPager = (ViewPager) view.findViewById(R.id.my_viewpager);
        mTabLayout = (TabLayout) view.findViewById(R.id.my_tablayout);
        return view;
    }

    @Override
    public void onResume()
    {
        super.onResume();

        MyActivity myActivity = (MyActivity) getActivity();
        myActivity.getSupportActionBar().setTitle(getString(R.string.my_title));

        mAdapter = new MyPagerAdapter(getChildFragmentManager(), myActivity);
        mViewPager.setAdapter(mAdapter);
        mViewPager.setOffscreenPageLimit(PagerConstants.OFFSCREEN_PAGE_LIMIT);
        mViewPager.setCurrentItem(mCurrentItem); // <-- Note the use of mCurrentItem here!
        mTabLayout.setupWithViewPager(mViewPager);
    }

    /**
     * Call this at any point before needed, for example before performing a FragmentTransaction.
     */    
    public void setCurrentItem(int currentItem)
    {
        mCurrentItem = currentItem;

        // This should be called in cases where onResume() is not called later,
        // for example if you only want to change the page in the ViewPager
        // when clicking a Button or whatever. Just omit if not needed.
        mViewPager.setCurrentItem(mCurrentItem); 
    }


}
Krøllebølle
  • 2,878
  • 6
  • 54
  • 79
  • In my case, I moved all the init lines (like setting adapter, listeners, etc.) from onResume() to onCreateView() and it worked – Variag Jul 06 '18 at 10:49
2

For me this worked setting current item after setting adapter

viewPager.setAdapter(new MyPagerAdapter(getSupportFragmentManager()));

viewPager.setCurrentItem(idx);

pagerSlidingTabStrip.setViewPager(viewPager);// assign viewpager to tabs
arun-r
  • 3,104
  • 2
  • 22
  • 20
2

I was working on this problem for one week and I realized that this problem happens because I was using home activity context in view pager fragments and we can only use context in fragment after it gets attached to activity..
When a view pager gets created, activity only attach to the first (0) and second (1) page. When you open the second page, the third page gets attached and so on! When you use setCurrentItem() method and the argument is greater than 1, it wants to open that page before it is attached, so the context in fragment of that page will be null and the application gets crashed! That's why when you delay setCurrentItem(), it works! At first it gets attached and then it'll open the page...

Dhairya Tripathi
  • 197
  • 2
  • 14
Mahdi Dahouei
  • 1,588
  • 2
  • 12
  • 32
1

I've done it this way to restore the current item:

@Override
protected void onSaveInstanceState(Bundle outState) {

    if (mViewPager != null) {
        outState.putInt(STATE_PAGE_NO, mViewPager.getCurrentItem());
    }

    super.onSaveInstanceState(outState);
}

@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {

    if (savedInstanceState != null) {
        mCurrentPage = savedInstanceState.getInt(STATE_PAGE_NO, 0);
    }

    super.onRestoreInstanceState(savedInstanceState);
}

@Override
protected void onRestart() {
    mViewPager.setCurrentItem(mCurrentPage);
         super.onRestart();
}
Szymon
  • 42,577
  • 16
  • 96
  • 114
  • The thing is im not restoring instance state, its just onStart and onResume method called. Ive got the position stored properly, aswell i looked into my Viewpager object at debug mode and it has mCurItem set properly, yet its still showing the first element – Maciej Boguta Oct 11 '13 at 11:39
  • I can't remember exactly but I think I has a similar problem and I used the code above. But maybe not exactly the same. – Szymon Oct 11 '13 at 11:41
1

By the time I call setCurrentItem() the view is about to be recreated. So in fact I invoke setCurrentItem() for the viewpager and afterwards the system calls onCreateView() and hence creates a new viewpager.

This is the reason for me why I do not see any changes. And this is the reason why a postDelayed() may help.

Theoretical solution: Postpone the setCurrentItem() invocation until the view has been recreated.

Practical solution: I have no clue for a stable and simple solution. We should be able to check if the class is about to recreate it's view and if that is the case postpone the invocation of setCurrentItem() to the end of onCreateView()

mikes
  • 2,323
  • 1
  • 16
  • 11
1

I use the dsalaj code as a reference. If necessary I share the code with the complete solution.

I also strongly recommend using ViewPager2

Solution

Both cases have to go within the Observer {}:

  1. First case: Initialize the adapter only when we have the first data set and not before, since this would generate inconsistencies in the paging. To the first data set we have to pass it as the argument of the Adapter.

  2. Second case: From the first change in the observable we would have from the second data sets onwards which have to be passed to the Adapter through a public method only if we have already initialized the adapter with a first data set.

GL

Braian Coronel
  • 22,105
  • 4
  • 57
  • 62
  • This continue happening on viewPager2 . it sucks – Gilberto Ibarra Apr 06 '21 at 21:09
  • I remember very well that this had worked for me brother. Try to debug the critical points. Then it evaluates how the adapter is initialized if the observable has data, and how the adapter sets the new data when the observable is updated – Braian Coronel Apr 07 '21 at 02:32
1

I was confused with the onActivityCreated() getting invoked for unrelated tab @Mahdi Arabpour was an eye opener for me :)

For me the problem was the third page (as elaborated by @Mahdi Arabpour above) was getting reconstructed when I click the second tab, etc and it was losing its data adapter, setting it again in onActivityCreted solves my problems:

    if (myXXRecyclerAdapter != null) {
        myXXRecyclerAdapter = new MyXXRecyclerAdapter(myStoredData);
        mRecyclerView.setAdapter(myXXRecyclerAdapter );
        return;
    }
Bakavani
  • 101
  • 5
-1

You need to call pager.setCurrentItem(activePage) right after pager.setAdapter(buildAdapter())

@Override
public void onResume() {
    if (pager.getAdapter() != null) {
        activePage=pager.getCurrentItem();
        Log.w(getClass().getSimpleName(), "pager.getAdapter()!=null");
        pager.setAdapter(null);

    }

    pager.setAdapter(buildAdapter());            
    pager.setCurrentItem(activePage);
}
Plugie
  • 1,289
  • 17
  • 25