ViewPager2 doesn't show correct Fragment on slow devices or when debugging

Solution 1:

I made a fresh example, using:

Android Studio 4.0.1
Build #AI-193.6911.18.40.6626763, built on June 25, 2020
Runtime version: 1.8.0_242-release-1644-b3-6222593 amd64
VM: OpenJDK 64-Bit Server VM by JetBrains s.r.o
Linux 5.4.0-7642-generic

I basically did:

File, New Project, min API 26 (so I can run on my Nexus 5X), and picked the Empty Activity template.

I then one by one, added your code shared here, with very minor changes (for starters, your Layout contains a constrain to a id/btn that is not there, so I assume you only pasted the relevant bits, check the layout xml in my repo)

Results

Either when using no debugger attached (but obviously a debug build) or when placing a breakpoint in createFragment(), I still observe the expected behavior.

I tested on:

  • Fast emulator (Pixel 3a with API 30)
  • Real Nexus 5X with Android 8.1

I noticed that the fragments are correctly created when needed (only when I "swipe"); this is due to the ExampleStateAdapter being a subclass of FragmentStateAdapter which has a similar behavior by design to the original FragmentStatePagerAdapter which was good for large sets of pages where the creation is "lazy".

You can test this project very easily, I put it online in GitHub here:

https://github.com/Gryzor/TabbedVp2

Conclusion

Are you sure there is no other issue with your app causing an unnecessary delay when the fragments are managed? Something else in your layout that may be causing the measure/layout pass to take longer?

Logs (filtering by frg)

Fresh App Start:

D/frg: createFragment: 0
D/frg A: onStart()
D/frg A: onResume()

All good here, the first fragment (0) is created and started. Notice no Fragment B is "pre-created"; this may be a design choice by the fragment adapter, didn't look into its source code. This behavior is different starting with the next swipe...

Swipe to reveal Fragment B (and hide A):

1 D/frg: createFragment: 1
2 D/frg B: onStart()
3 D/frg: createFragment: 2
4 D/frg A: onPause()
5 D/frg B: onResume()

The only "strange" thing here, is the call to createFragment: 2 in line 3. Let's see line by line:

  1. Fragment 0 is the current, the next is "1" so the call is correct.

  2. Fragment 1 ("b") is now being created and "started".

  3. The State Adapter is preemptively creating Framgment "C" (number 2), but notice it's not yet Started or Resumed. It did NOT do this for Fragment B when we were displaying Fragment A, this is by design I think, so if/when you show the first item, nothing is preemptively created; I wasn't aware of this, and I didn't inspect the source code, but that's how it behaves. From now on, every time a new fragment is created, the next is "pre-created" behind the scenes so it's ready to be started.

  4. As Fragment B is started, Fragment B is now paused as it goes off-screen.

  5. Fragment B is finally resumed (After it was being started) as it's now visible.

Now Swipe back to "A" (for fun)

D/frg B: onPause()
D/frg A: onResume()

Not much to see, B is now paused and A is resumed, since it was created and started.

Let's swipe back to "B"

D/frg A: onPause()
D/frg B: onResume()

The inverse happens, nothing to see here.

Now let's swipe to C

1 D/frg C: onStart()
2 D/frg: createFragment: 3
3 D/frg B: onPause()
4 D/frg C: onResume()
  1. C was pre-created, but never started, it is now Started.
  2. Again, the adapter is pre-creating the next one ("D").
  3. B is going away, so it's paused.
  4. Finally C is resumed and visible.

Final Test, keep swiping to D...

1 D/frg D: onStart()
2 D/frg: createFragment: 4
3 D/frg A: onStop()
4 D/frg C: onPause()
5 D/frg D: onResume()

There was a reason why I wanted to reach "D" (or rather, be far from A) :)

Let's look at these logs

  1. D was pre-created in the previous step in "1", so it's now started.
  2. E is now being pre-created in case we swipe again.
  3. A is now stopped and prone to be destroyed if needed. Since we've now gone beyond the default (3 total which is current + and - one if I correctly recall), the adapter is "stopping" unused Fragments to reclaim memory if needed. So A is semi-gone.
  4. Now we pause C as usual, since it's going away in favor of D.
  5. ...and Resume D, which was recently started.

At this point, you get the flow. If you keep going to "E", then Fragment "B" is going to be stopped:

D/frg E: onStart()
D/frg: createFragment: 5
D/frg B: onStop()
D/frg D: onPause()
D/frg E: onResume()

Business as usual.

UPDATE

I decided to test one more thing so I pushed this commit. It adds a button that will select the 10th fragment (which happens to be "K").

  1. App Starts, A is Visible.
D/frg: createFragment: 0
D/frg A: onStart()
D/frg A: onResume()
  1. Button is Pressed, K is now visible (after an animation)
01 D/frg: createFragment: 7
02 D/frg H: onStart()
03 D /frg: createFragment: 8
04 D/frg I: onStart()
05 D/frg: createFragment: 9
06 D/frg J: onStart()
07 D/frg: createFragment: 10
08 D/frg K: onStart()
09 D/frg: createFragment: 11
10 D/frg A: onPause()
11 D/frg A: onStop()
12 D/frg H: onStop()
13 D/frg K: onResume()

The position we're after is 10, it's really the 11th element because it's "zero based" (so A is zero, not 1).

The StateAdapter is trying to be smart (?) and is creating Fragment at position 7 (H), 8 (I), 9 (J), 10 (K) and 11 (L) as seen in lines 1, 3, 5, 7, and 9.

Here comes the Adapter's "intelligence" stupidity:

It goes one by one and starts them: H in line 2, I in line 4, J in line 6, and K in line 8.

Then it stops A (in Line 11) because it's outside of the scope of the state to maintain, and also stops H because for some reason it decided to start it preemptively, only to realize that it's no longer in "scope", so H gets stopped in line 12 too. Very inefficient here.

Finally K is "Resumed" since it's... well.. visible!

And here is where the weird behavior I cannot explain starts: My questions to Google would be (possibly easy to answer if we take a look at the state adapter's source code but... who wants to do that now?).

  • Why Is Fragment H started then stopped? My Theory is that the logic is triggered and H is exactly as far from "K" as "A" was from "D" (it has two other frags in between) so that's why it gets stopped afterwards, but why was it started?
  • Why are fragments I, J (the ones in between H and K) Started too, BUT not stopped?

I assume this has to do with the same reason why when you swipe from C to D, A is stopped, but B and C aren't, they are paused. B would already been paused, and C gets paused when you go to D and A is stopped, but in this "jump" from A to K, these fragments are not PAUSED, which is a different state and possibly a bug. In a way, they are Started and not Resumed, so that's a "paused state", and that's why this works and is "kinda consistent".

I would have expected at the very least that I, and J be at least paused since that's how they would be if you swipe one by one, but the truth is they were never resumed so there's no reason to call Pause.

In the end, after this big "jump" from A to K, the fragments are in their expected state for being in Fragment K:

  1. Nothing before (and including) "H" is created (or started/resumed). Correct.
  2. Forwards wise (L and beyond) only "L" is pre-created (line 9 in the previous log where Fragment "11" is created but not started). Correct.
  3. Fragments I, and J are "Started" and ready to go, but were never resumed so far (so they are paused). If you swipe "back" to J, it will be resumed (faster than having to create/start).

Well, there you have the behavior "documented" (not explained, since I didn't dig any further).

I hope this helps you clarify if anything, what the adapters are trying to do with your fragments and lifecycle. Now in older APIs and slow devices, this whole thing may take a few milliseconds (vs. very very few on newer devices).

Update II

If you disable the animation (either via the TabLayoutMediator as you found out, or the viewPager.setCurrentItem(nn, false) then this problem doesn't occur. This leads me to believe the "animation/transaction" system is being triggered as the "animation" to "go to the selected item" is being constructed and executed.

Meanwhile a Google Issue has been filed.