Reputation: 8979
I'm using ViewPager2
with TabLayout
and FragmentStateAdapter
to display Fragments in tabs. I found out that when I run it on slower devices (ie. Nexus 5X) or when I put a breakpoint to createFragment()
method, after I change to some later tab with Fragment, that is not created yet, Fragment on last position is shown.
In example I have 20 items. I select tab on position 15. Fragments on positions 15, 14, 16, 17, 18, 19 and 20 are created (createFragment()
is called). I would expect to create only 14, 15, 16 to be created. On slow devices or when stopping debugger inside createFragment()
method, Fragment on position 20 is shown.
This issue is not happening on the devices that are not slowed down.
MainActivity:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initViewPager2WithFragments()
}
private fun initViewPager2WithFragments() {
val viewPager2: ViewPager2 = findViewById(R.id.viewpager)
val adapter = ExampleStateAdapter(supportFragmentManager, lifecycle)
viewPager2.adapter = adapter
val tablayout: TabLayout = findViewById(R.id.tablayout)
val names: Array<String> = arrayOf(
"Fragment A",
"Fragment B",
"Fragment C",
"Fragment D",
"Fragment E",
"Fragment F",
"Fragment G",
"Fragment H",
"Fragment I",
"Fragment J",
"Fragment K",
"Fragment L",
"Fragment M",
"Fragment N"
)
TabLayoutMediator(tablayout, viewPager2) { tab, position ->
tab.text = names[position]
}.attach()
}
}
ExampleStateAdapter:
class ExampleStateAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) :
FragmentStateAdapter(fragmentManager, lifecycle) {
override fun getItemCount(): Int {
return 14
}
override fun createFragment(position: Int): Fragment {
Log.d("frg", "createFragment: $position")
return when (position) {
0 -> SomeFragment.newInstance("A")
1 -> SomeFragment.newInstance("B")
2 -> SomeFragment.newInstance("C")
3 -> SomeFragment.newInstance("D")
4 -> SomeFragment.newInstance("E")
5 -> SomeFragment.newInstance("F")
6 -> SomeFragment.newInstance("G")
7 -> SomeFragment.newInstance("H")
8 -> SomeFragment.newInstance("I")
9 -> SomeFragment.newInstance("J")
10 -> SomeFragment.newInstance("K")
11 -> SomeFragment.newInstance("L")
12 -> SomeFragment.newInstance("M")
13 -> SomeFragment.newInstance("N")
else -> SomeFragment.newInstance("0")
}
}
}
SomeFragment:
class SomeFragment : Fragment() {
var TAG = "frg 0"
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
TAG = "frg ${arguments?.getString(CATEGORY)}"
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_some, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.findViewById<TextView>(R.id.textView).apply {
text = arguments?.let {
it.getString(CATEGORY, "")
} ?: "not found"
}
}
override fun onStart() {
Log.d(TAG, "onStart()")
super.onStart()
}
override fun onResume() {
Log.d(TAG, "onResume()")
super.onResume()
}
override fun onPause() {
Log.d(TAG, "onPause()")
super.onPause()
}
override fun onStop() {
Log.d(TAG, "onStop()")
super.onStop()
}
companion object {
fun newInstance(
categoryName: String
) = SomeFragment().apply {
arguments = bundleOf(
CATEGORY to categoryName
)
}
}
}
fragment_some.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/ConstraintLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#CDDC39"
tools:context=".fragments.SomeFragment">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Fragment"
android:textSize="30sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewpager"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tablayout" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tablayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/btn"
app:tabMode="scrollable" />
</androidx.constraintlayout.widget.ConstraintLayout>
dependencies:
// ViewPager2
implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01'
// CardView
implementation 'androidx.cardview:cardview:1.0.0'
// Tablayout + other stuff...
implementation 'com.google.android.material:material:1.2.1'
I tried to put some logs and see what's happening. I have 14 Fragments in the list and I select tab on position 9.
Normal scenario (not stopping debugger in createFragment()
method):
2020-09-22 15:10:33.515 26627-26627/com.example.viewpager2_example_2 D/frg: createFragment: 5
2020-09-22 15:10:33.534 26627-26627/com.example.viewpager2_example_2 D/frg F: onStart()
2020-09-22 15:10:33.552 26627-26627/com.example.viewpager2_example_2 D/frg D: onStop()
2020-09-22 15:10:33.573 26627-26627/com.example.viewpager2_example_2 D/frg: createFragment: 6
2020-09-22 15:10:33.587 26627-26627/com.example.viewpager2_example_2 D/frg G: onStart()
2020-09-22 15:10:33.597 26627-26627/com.example.viewpager2_example_2 D/frg: createFragment: 7
2020-09-22 15:10:33.609 26627-26627/com.example.viewpager2_example_2 D/frg H: onStart()
2020-09-22 15:10:33.633 26627-26627/com.example.viewpager2_example_2 D/frg C: onStop()
2020-09-22 15:10:33.636 26627-26627/com.example.viewpager2_example_2 D/frg: createFragment: 8
2020-09-22 15:10:33.647 26627-26627/com.example.viewpager2_example_2 D/frg I: onStart()
2020-09-22 15:10:33.752 26627-26627/com.example.viewpager2_example_2 D/frg B: onStop()
2020-09-22 15:10:33.967 26627-26627/com.example.viewpager2_example_2 D/frg: createFragment: 9
2020-09-22 15:10:33.972 26627-26627/com.example.viewpager2_example_2 D/frg A: onPause()
2020-09-22 15:10:33.973 26627-26627/com.example.viewpager2_example_2 D/frg A: onStop()
2020-09-22 15:10:34.018 26627-26627/com.example.viewpager2_example_2 D/frg F: onStop()
2020-09-22 15:10:34.023 26627-26627/com.example.viewpager2_example_2 D/frg I: onResume()
Problematic scenario (stopping debugger in createFragment()
method for 1-2 seconds each time):
2020-09-22 15:14:26.419 26627-26627/com.example.viewpager2_example_2 D/frg: createFragment: 5
2020-09-22 15:14:28.551 26627-26627/com.example.viewpager2_example_2 D/frg F: onStart()
2020-09-22 15:14:28.577 26627-26627/com.example.viewpager2_example_2 D/frg D: onStop()
2020-09-22 15:14:28.602 26627-26627/com.example.viewpager2_example_2 D/frg: createFragment: 6
2020-09-22 15:14:29.591 26627-26627/com.example.viewpager2_example_2 D/frg G: onStart()
2020-09-22 15:14:29.615 26627-26627/com.example.viewpager2_example_2 D/frg: createFragment: 7
2020-09-22 15:14:30.962 26627-26627/com.example.viewpager2_example_2 D/frg H: onStart()
2020-09-22 15:14:30.999 26627-26627/com.example.viewpager2_example_2 D/frg C: onStop()
2020-09-22 15:14:31.003 26627-26627/com.example.viewpager2_example_2 D/frg B: onStop()
2020-09-22 15:14:31.006 26627-26627/com.example.viewpager2_example_2 D/frg: createFragment: 8
2020-09-22 15:14:31.922 26627-26627/com.example.viewpager2_example_2 D/frg I: onStart()
2020-09-22 15:14:31.956 26627-26627/com.example.viewpager2_example_2 D/frg A: onPause()
2020-09-22 15:14:31.957 26627-26627/com.example.viewpager2_example_2 D/frg A: onStop()
2020-09-22 15:14:31.965 26627-26627/com.example.viewpager2_example_2 D/frg: createFragment: 9
2020-09-22 15:14:33.210 26627-26627/com.example.viewpager2_example_2 D/frg J: onStart()
2020-09-22 15:14:33.240 26627-26627/com.example.viewpager2_example_2 D/frg G: onStop()
2020-09-22 15:14:33.248 26627-26627/com.example.viewpager2_example_2 D/frg: createFragment: 10
2020-09-22 15:14:34.247 26627-26627/com.example.viewpager2_example_2 D/frg K: onStart()
2020-09-22 15:14:34.276 26627-26627/com.example.viewpager2_example_2 D/frg F: onStop()
2020-09-22 15:14:34.280 26627-26627/com.example.viewpager2_example_2 D/frg: createFragment: 11
2020-09-22 15:14:35.286 26627-26627/com.example.viewpager2_example_2 D/frg L: onStart()
2020-09-22 15:14:35.316 26627-26627/com.example.viewpager2_example_2 D/frg H: onStop()
2020-09-22 15:14:35.320 26627-26627/com.example.viewpager2_example_2 D/frg: createFragment: 12
2020-09-22 15:14:36.407 26627-26627/com.example.viewpager2_example_2 D/frg M: onStart()
2020-09-22 15:14:36.435 26627-26627/com.example.viewpager2_example_2 D/frg I: onStop()
2020-09-22 15:14:36.439 26627-26627/com.example.viewpager2_example_2 D/frg: createFragment: 13
2020-09-22 15:14:37.632 26627-26627/com.example.viewpager2_example_2 D/frg N: onStart()
2020-09-22 15:14:37.656 26627-26627/com.example.viewpager2_example_2 D/frg J: onStop()
Anyone can explain what sort of sorcery is happening here inside ViewPager2
?
Upvotes: 6
Views: 2113
Reputation: 94
I solved setting offscreenPageLimit some value, in my case I have 6 tabs and set the value to 3, it´s work really well. Now the performance is perfect!
Upvotes: 0
Reputation: 94
When you create TabLayoutMediator set smoothscroll false
TabLayoutMediator(mTabLayout, mViewPager2, autorefresh = true, smoothScroll= false, TabLayoutMediator.TabConfigurationStrategy{ //your code })
Upvotes: 0
Reputation: 27226
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)
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:
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
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?
frg
)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...
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:
Fragment 0 is the current, the next is "1" so the call is correct.
Fragment 1 ("b") is now being created and "started".
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.
As Fragment B is started, Fragment B is now paused as it goes off-screen.
Fragment B is finally resumed (After it was being started) as it's now visible.
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.
D/frg A: onPause()
D/frg B: onResume()
The inverse happens, nothing to see here.
1 D/frg C: onStart()
2 D/frg: createFragment: 3
3 D/frg B: onPause()
4 D/frg C: onResume()
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
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.
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").
A
is Visible.D/frg: createFragment: 0
D/frg A: onStart()
D/frg A: onResume()
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?).
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:
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).
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.
Upvotes: 2