Status Update
Comments
[Deleted User] <[Deleted User]> #2
Please include a sample project that reproduces your issue.
[Deleted User] <[Deleted User]> #3
Sample project attached. Just add to plain project this dependencies allow to reproduce.
def emoji2_version = "1.1.0-beta01"
implementation "androidx.emoji2:emoji2:$emoji2_version"
def lifecycle_version = "2.5.0-alpha01"
implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version"
jg...@google.com <jg...@google.com>
[Deleted User] <[Deleted User]> #4
The reason may be related to 2.4.0
of lifecycle-process
as workaround.
jg...@google.com <jg...@google.com> #5
Yes, this is due to this change, as well as the fact that EmojiCompatInitializer has a ProcessLifecycleInitializer as a dependency and also re-calls it, but in manual mode.
jg...@google.com <jg...@google.com> #6
ProcessLifecycleInitializer
is expected to already be initialized (via the manifest provider
for App Startup) by the time that code runs - App Startup shouldn't be calling create
if that component has already been initialized, hence you shouldn't get any exception. We'll take a look on where things are breaking down though.
jg...@google.com <jg...@google.com> #7
Thanks for the answer.
As I understand EmojiCompatInitializer has ProcessLifecycleInitializer as a appInitializer.initializeComponent(ProcessLifecycleInitializer.class)
Maybe it would be more correct to create a bug in emoji-2, but let it be here.
jg...@google.com <jg...@google.com> #8
This issue isn't related to emoji2, it could be reproduced easily with the lifecycle-process
(2.4.0 -> 2.5.0-alpha01) only.
[Deleted User] <[Deleted User]> #9
Branch: androidx-main
commit 97993ad954cb98211ef52b6e26c7877dbdeeec1c
Author: Jeremy Woods <jbwoods@google.com>
Date: Tue Feb 01 17:06:11 2022
Bump lifecycle process start-up dependency to 1.1.1
Bumping the startup dependency to include aosp/1855769.
RelNote: "Updated `lifecycle-process` to depend on [Startup 1.1.1](/jetpack/androidx/releases/startup#1.1.1) to ensure that fixes that prevent `ProcessLifecycleInitializer` from throwing a `StartupException` are available by default."
Bug: 216490724
Test: tested in sample app
Change-Id: Ib01dfbba1d63aa03e43e09ee8886cc76e1050e1b
M lifecycle/lifecycle-process/build.gradle
[Deleted User] <[Deleted User]> #10
This has been fixed internally and will be released in the Lifecycle 2.5.0-alpha02
release.
jg...@google.com <jg...@google.com> #11
When will start-up 1.1.1
release?
[Deleted User] <[Deleted User]> #12
It should be out before the end of the week.
jg...@google.com <jg...@google.com>
jg...@google.com <jg...@google.com> #13
- the same instance of ViewPagerFragment is being used for various back-stack entries
- multiple instances of FragmentStateAdapter are being created at each ViewPagerFragment view inflation, and each has their post-grace-period gcFragments() scheduled
- the same page Fragment instance reference is passed to all FragmentStateAdapter instances via restoreState, but can only be bound to one ViewHolder (the current visible one)
- once grace-period ends in non-visible FragmentStateAdapter instances, they see nothing is bound to their ViewHolders and garbage-collect the page Fragment
I will look further into the correct solution, potentially cancelling gcFragments() if a FragmentStateAdapter gets detached from a ViewPager2 (e.g. at ViewPagerFragment view destruction).
This would require a code, change, though, so for now, you can probably work around this by not re-using ViewPagerFragment instances, but creating a new one each time you add it to the stack. There might be a need for passing relevant state, e.g. current page.
Raw debugging notes below in case helpful. I reproduced the issue by switching multiple times between buttons 1 and 2.
***
# how many adapter instances exist
️$ cat ff2 | grep -E "FragmentStateAdapter@[^\}]+" -o | sort -u
FragmentStateAdapter@5,446
FragmentStateAdapter@5,474
FragmentStateAdapter@5,484
# how many adapter instances get their state restored
️$ cat ff2 | grep restoreState | grep -E "FragmentStateAdapter@[^\}]+" -o | sort -u
FragmentStateAdapter@5,474
FragmentStateAdapter@5,484
# where are Fragment pages created
️$ cat ff2 | grep createFragment -C1
{androidx.viewpager2.adapter.FragmentStateAdapter@5,446}.mFragments will be accessed at androidx.viewpager2.adapter.FragmentStateAdapter.ensureFragment(FragmentStateAdapter.java:248)
Breakpoint reached at com.test.myapplication.VPAdapter.createFragment(ViewPagerFragment.kt:161)
{androidx.viewpager2.adapter.FragmentStateAdapter@5,446}.mFragments will be accessed at androidx.viewpager2.adapter.FragmentStateAdapter.ensureFragment(FragmentStateAdapter.java:252)
# confirming that instance 446 is the only adapter creating the views; others are getting it from previously restored state
# for reference, FragmentStateAdapter.java:248 = "if (!mFragments.containsKey(itemId)) {"
# for reference, FragmentStateAdapter.java:252 = "mFragments.put(itemId, newFragment);"
️$ cat ff2 | grep ensureFragment | sort
{androidx.viewpager2.adapter.FragmentStateAdapter@5,446}.mFragments will be accessed at androidx.viewpager2.adapter.FragmentStateAdapter.ensureFragment(FragmentStateAdapter.java:248)
{androidx.viewpager2.adapter.FragmentStateAdapter@5,446}.mFragments will be accessed at androidx.viewpager2.adapter.FragmentStateAdapter.ensureFragment(FragmentStateAdapter.java:252)
{androidx.viewpager2.adapter.FragmentStateAdapter@5,474}.mFragments will be accessed at androidx.viewpager2.adapter.FragmentStateAdapter.ensureFragment(FragmentStateAdapter.java:248)
{androidx.viewpager2.adapter.FragmentStateAdapter@5,484}.mFragments will be accessed at androidx.viewpager2.adapter.FragmentStateAdapter.ensureFragment(FragmentStateAdapter.java:248)
# where is the state with a page Fragment reference coming from
Breakpoint reached
at androidx.viewpager2.adapter.FragmentStateAdapter.restoreState(FragmentStateAdapter.java:515)
at androidx.viewpager2.widget.ViewPager2.restorePendingState(ViewPager2.java:337)
at androidx.viewpager2.widget.ViewPager2.dispatchRestoreInstanceState(ViewPager2.java:362)
at android.view.View.restoreHierarchyState(View.java:17684)
at androidx.fragment.app.Fragment.restoreViewState(Fragment.java:539)
at androidx.fragment.app.FragmentManagerImpl.moveToState(FragmentManagerImpl.java:907)
at androidx.fragment.app.FragmentManagerImpl.moveFragmentToExpectedState(FragmentManagerImpl.java:1238)
at androidx.fragment.app.FragmentManagerImpl.moveToState(FragmentManagerImpl.java:1303)
at androidx.fragment.app.BackStackRecord.executeOps(BackStackRecord.java:439)
at androidx.fragment.app.FragmentManagerImpl.executeOps(FragmentManagerImpl.java:2076)
at androidx.fragment.app.FragmentManagerImpl.executeOpsTogether(FragmentManagerImpl.java:1866)
at androidx.fragment.app.FragmentManagerImpl.removeRedundantOperationsAndExecute(FragmentManagerImpl.java:1821)
at androidx.fragment.app.FragmentManagerImpl.execPendingActions(FragmentManagerImpl.java:1727)
at androidx.fragment.app.FragmentManagerImpl$2.run(FragmentManagerImpl.java:150)
at android.os.Handler.handleCallback(Handler.java:790)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:164)
at android.app.ActivityThread.main(ActivityThread.java:6494)
at java.lang.reflect.Method.invoke(Method.java:-1)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807)
# how many Fragment instances go through restoreViewState
️$ cat ff4 | grep "restoreViewState. fragment="
restoreViewState: fragment=ViewPagerFragment{d4490de (a6cfe7d3-5376-4015-8357-2408442116ee) id=0x7f070076 Frag1}
restoreViewState: fragment=BaseFragment{de52066 (f5afa133-40d7-45c1-8193-6ce2d4cb5489) f0}
restoreViewState: fragment=BaseFragment{de52066 (f5afa133-40d7-45c1-8193-6ce2d4cb5489) f0}
restoreViewState: fragment=ViewPagerFragment{d4490de (a6cfe7d3-5376-4015-8357-2408442116ee) id=0x7f070076 Frag1}
restoreViewState: fragment=BaseFragment{de52066 (f5afa133-40d7-45c1-8193-6ce2d4cb5489) f0}
restoreViewState: fragment=ViewPagerFragment{d4490de (a6cfe7d3-5376-4015-8357-2408442116ee) id=0x7f070076 Frag1}
# where are ViewPagerFragment instances created in the sample
```
var fragment: Fragment? = supportFragmentManager.findFragmentByTag("Frag$fragmentToOpen")
if(fragment == null) {
fragment = when(fragmentToOpen) {
1 -> ViewPagerFragment()
2 -> BlankFragment()
else -> throw IllegalArgumentException()
}
}
```
[Deleted User] <[Deleted User]> #14
Looking forward to erasing all of this ugly code on next update!
[Deleted User] <[Deleted User]> #15
binding.gamePagerRoot.apply {
// Set the Adapter for our Fragment
adapter = gamePagerAdapter
// Do not allow user touch for this ViewPager2 to affect its scroll, should only be scrolled programmatically
isUserInputEnabled = false
// Set the Page Transformation
setPageTransformer(genStackedPageTransformer2())
// Listen to page change calbacks
registerOnPageChangeCallback(onPageChangeCallback)
// Customize the RecyclerView child
val recyclerView = getChildAt(0) as RecyclerView
// Remove the overscroll shaddow
recyclerView.overScrollMode = RecyclerView.OVER_SCROLL_NEVER
// Allow nested scrolling
recyclerView.isNestedScrollingEnabled = true
// If we are on a different page as specified by our ViewModel, make sure to scroll to the page instantly (no animation)
if(gamePagerVM?.gamePageIds?.size ?: 1 > 1) {
recyclerView.viewTreeObserver.addOnGlobalLayoutListener(object: ViewTreeObserver.OnGlobalLayoutListener{
override fun onGlobalLayout() {
if(recyclerView.childCount >= gamePagerVM?.currPage ?: 0) {
viewTreeObserver.removeOnGlobalLayoutListener(this)
// If the current page is not 0, set the current page
setCurrentItem(gamePagerVM?.currPage ?: 0, false)
}
}
})
}
}
ap...@google.com <ap...@google.com> #16
Branch: androidx-master-dev
commit 7166ba571d594baa9016e02c3b5fe967da0b8ec0
Author: Jakub Gielzak <jgielzak@google.com>
Date: Tue Aug 27 16:20:54 2019
Fix for FragmentStateAdapter#gcFragments()
Fix for an issue with FragmentStateAdapter garbage collection mechanism.
The bug manifested itself in the following scenario:
- ViewPager2 host Fragment view recreated 3+ times in a window < 10s,
- Host Fragment instances added to the back stack,
- ViewPager2 adapter set to null in onViewDestoyed (too late as view
state already saved, but affecting PageFragment book-keeping).
The fix adds another criteria to gcFragments() in case a
PageFragment instance gets taken over by a later created instance of
FragmentStateAdapter.
Bug: 139095195
Test: ./gradlew viewpager2:connectedCheck
Change-Id: If42bc4e65e8fe52cc0dcd9bbf67a0c55d3c535b1
A viewpager2/src/androidTest/java/androidx/viewpager2/widget/HostFragmentBackStackTest.kt
M viewpager2/src/main/java/androidx/viewpager2/adapter/FragmentStateAdapter.java
ap...@google.com <ap...@google.com> #17
Branch: androidx-master-dev
commit 39daf73f75d04b1ba52b007781355595430f8dab
Author: Jakub Gielzak <jgielzak@google.com>
Date: Tue Aug 27 16:20:16 2019
Made test FragmentAdapter more flexible.
Needed for the next CL.
Bug: 139095195
Test: ./gradlew viewpager2:connectedCheck
Change-Id: I3aa4b8a131d3d7bca248f94641c5ee643c6f1819
M viewpager2/src/androidTest/java/androidx/viewpager2/widget/BaseTest.kt
M viewpager2/src/androidTest/java/androidx/viewpager2/widget/swipe/FragmentAdapter.kt
Description
Creating Sample app to verify, but in case this is already a known issue