I'm trying to migrate to FragmentStateAdapter but got a problem with showing fragments.
When I first load the adapter, the createFragment() has been called for each Fragment. So from the beginning, I'm seeing the fragments with the correct data. But when I swipe back and forth - the first fragment start to be a blank white screen and if I do one more swipe again, the second page has an empty blank page too.
I tried to set a background color as a def param in the parent view to check is the fragment is still has been restored from the fragment manager. The result shows that yes - they are there but because I'm using arguments to declare the type that will be displayed in this fragment - after recycling the arguments are not exists anymore and that's why I see the blank white page.
Correct me if I'm wrong and what solution will fit better here?
Thanks
class ReviewContractAdapter(activity: FragmentActivity, private val cacheDirAbsolutePath: String):
FragmentStateAdapter(activity) {
companion object {
const val REVIEW_CONTRACT_NUMBER_OF_PAGES = 2
const val CONTRACT = "Contract"
const val PAD = "PAD"
}
override fun getItemCount(): Int = REVIEW_CONTRACT_NUMBER_OF_PAGES
override fun createFragment(position: Int): Fragment {
return PdfViewerFragment().apply {
arguments = Bundle().apply {
when (position) {
0 -> putString(CONTRACT_FILE_PATH, cacheDirAbsolutePath + CONTRACT_PATH)
else -> putString(PAD_FILE_PATH, cacheDirAbsolutePath + PAD_PATH)
}
}
}
}
}
Using Activity to init the Adapter
Nothing special there
review_contract_viewpager.adapter = adapter
TabLayoutMediator(review_contract_tabs, review_contract_viewpager) { tab, position ->
tab.text = when (position) {
0 -> ReviewContractAdapter.CONTRACT
else -> ReviewContractAdapter.PAD
}
}.attach()
Related
I'm using custom Pages in my ViewPager and entire App is based on those screens.
There are 2 main abstract functions in those Pages.
First one is getScreen() which suppose to copy function of onCreateView from Fragment. Its called inside ViewPagerAdapters function to initialize layout for that screen.
Example from adapter:
override fun instantiateItem(container: ViewGroup, position: Int): View {
val page = pageList[position]
val layout = page.getScreen(container)
App.log("flowSwitch: instantiateItem: getScreen: ${page::class.java}")
container.addView(layout)
page.screenLayoutWasInitialized = true
return layout
}
Another function is onScreenSwitched(). This one suppose to be called only if I switch to the screen manually by swiping/clicking on tab/calling it in code to get into next screen.
There is initializing some values for Views, sometimes based on payload provided from previous screens.
I call this onScreenSwitched() function inside switchScreen() function which is part of my Navigation class. I just pass there screen name and payload. Its always called after mainViewPager.setCurrentItem(index, useAnim).
Example:
fun switchScreen(
screen: Class<out FlowScreen>,
payload: List<ScreenPayload>? = null,
action: (() -> Unit)? = null,
useAnim: Boolean = true,
){
App.log("AppNavigationFlow: MainTabActivity: switchScreen: $screen")
try {
val index = mainFlowList.indexOfFirst { item -> item::class.java == screen }
App.log("AppNavigationFlow: MainTabActivity: switchScreen: index: $index")
if (index >= 0){
delayedScreenSelection {
mainTabLayout.getTabAt(index)?.select()
App.log("AppNavigationFlow: MainTabActivity: switchScreen: pageNameAtPos: ${mainViewPager.adapter?.getPageTitle(index)}")
mainViewPager.setCurrentItem(index, useAnim)
mainPagerAdapter.getPageAtPos(index)?.apply {
App.log("AppNavigationFlow: MainTabActivity: switchScreen: page: ${this::class.java}")
mainCurrentPage?.apply {
setScreenVisibleState(false)
resetToDefault()
removeBottomSheet(false)
this#MainTabActivity.removeBottomSheet(false)
}
mainCurrentPage = this
mainCurrentPage?.apply {
setScreenVisibleState(true)
clearPayload()
clearAction()
}
payload?.let { mainCurrentPage?.sendPayload(payload) }
action?.let { mainCurrentPage?.setAction(action) }
onScreenSwitched()
onPageChanged()
}?:kotlin.run {
setTabsEnabled(true)
}
}
} else {
setTabsEnabled(true)
}
}catch (e: IndexOutOfBoundsException){
App.log("AppNavigationFlow: MainTabActivity: switchScreen: $e")
setTabsEnabled(true)
}
}
98% of users are getting always called getScreen function before onScreenSwitched function, therefore my layout is completely initialized by that time onScreenSwitched is called.
But for 2% of users, they are getting kotlin.UninitializedPropertyAccessException because for example I'm trying to setup text for Button which was not initialized yet in getScreen function.
How to prevent this? I'm not sure if ViewPager should allow that to happen. How can setContentView ignore instantiateItem call if layout was not initialized yet for that screen I'm switching to? I ditched Fragments because of this bug happening in Fragments too and its happening again with fully customized logic. How can I build something functional when I cant even rely on basic native components to work as it suppose to at first place? There is possibly something I'm missing but 98% of time its working and I personally cant simulate those crashes but I want to fix it for those 2% of users.
Example usage in Page:
private lateinit var toolbarTitle: TextView
private lateinit var acceptButton: LoadingButton
override fun getScreen(collection: ViewGroup): View {
val layout = CustomResources.inflateLayout(inflater, l, collection, false) as ViewGroup
toolbarTitle = layout.findText(R.id.actionbarTitle)
acceptButton = layout.findViewById(R.id.acceptButton)
return layout
}
override fun onScreenSwitched() {
super.onScreenSwitched()
acceptButton.setText(if(payload.ok) "Yes" else "No")
}
Is there a way to navigate from a child fragment of ViewPager2 to another fragment?
Let's say I have a parent fragment [ParentFragment] containing only a ViewPager2 and a TabLayout. I have 2 children fragments [ChildrenFragment1 and ChildrenFragment2] created with FragmentStateAdapter. I want to navigate from ChildrenFragment2 to another FragmentB so that when i navigate back from FragmentB, to show position of the ParentFragment to ChildFragment2?
ex: ChildFragment1 -> ChildFragment2 -> FragmentB -> ChildFragment2
I get:
java.lang.IllegalArgumentException: Navigation action/destination id:actionChildFragment2_to_FragmentB cannot be found from the current destination
I found a solution, i keep the current page in a ViewModel LiveData property, setting it on page change inside the ParentFragment:
binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(page: Int) {
viewModel.updatePageSelected(page)
}
})
and observe the page change inside the ParentFragment:
subscribeToLiveData(viewModel.viewPagerSelected) { page ->
binding.viewPager.setCurrentItem(page, false)
}
The ParentViewModel:
private val _viewPagerSelected = MutableLiveData<Int>()
val viewPagerSelected: LiveData<Int> = _viewPagerSelected
fun updatePageSelected(newSelection: Int) {
_viewPagerSelected.value = newSelection
}
I have basic ViewPager2 with Tablayout - and in each page I have different fragments.
When I need to open this view NOT from first (default) tab I'm doing smth like this:
viewPager.currentItem = selectedTabPosition
This code select tab, but inside it is opening fragment from first tab! Only when I'm selecting the tabs by tapping on it - I can see the right fragment in each tabs.
I've also try to select with Tablayout like this:
tabLayout.getTabAt(position)?.select()
But this code doesn't help and also work with this bug.
Also I've try to set viewPager.currentItem with post / postDelay - but this doesn't work too.
Maybe I've lost something? Or it is a bug in ViewPager2 ?
(EDIT - ViewPager code)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupPagerAdapter()
}
private fun setupPagerAdapter() {
val adapter = MainDocumentScreenPagerAdapter(this)
binding?.viewPager?.letUnit {
it.adapter = adapter
binding?.tabsPagerView?.attachViewPager(requireContext(), it, adapter)
// set tab
it.currentItem = params.pageType.ordinal
}
Adapter code
class MainDocumentScreenPagerAdapter (fragment: Fragment) : ViewPager2TitleAdapter(fragment) {
override fun getItemCount(): Int = DocumentPageType.values().size
override fun createFragment(position: Int): Fragment {
val pageType = DocumentPageType.values().firstOrNull { it.ordinal == position } ?: throw IllegalStateException()
val params = DocumentListFragment.createParams(pageType)
return DocumentListFragment.newInstance(params)
}
override fun getPageTitle(position: Int): Int? {
return when (position) {
DocumentPageType.ALL.ordinal -> DocumentPageType.ALL.title
DocumentPageType.SIGN.ordinal -> DocumentPageType.SIGN.title
DocumentPageType.ACCEPT.ordinal -> DocumentPageType.ACCEPT.title
DocumentPageType.CONFIRM.ordinal -> DocumentPageType.CONFIRM.title
DocumentPageType.REJECT.ordinal -> DocumentPageType.REJECT.title
else -> null
}
}
Where ViewPager2TitleAdapter is :
abstract class ViewPager2TitleAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
abstract fun getPageTitle(position: Int): Int?
DocumentListFragment inside it create view, based on param object.
I've also try to create adapter inside OnCreate - but it doesn't affect this case.
The last, but not least - when I've try to open tab, which is out from screen (I've scrollable tabs) - viewPager open selected tab with correct fragment on it.. So, the problem occurs only when I've try to open first 4 tabs (look at the image), which is showing on screen. Bit starting from 5 and next tabs - has selected correct.
So, the decision is in this line of code:
it.setCurrentItem(params.pageType.ordinal, false)
but I was doing it like this:
it.currentItem = params.pageType.ordinal
boolean false make magic in this case - It disable smooth scroll. I've got it from this answer about ViewPager2:
https://stackoverflow.com/a/67319847/4809482
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)
}
How can be that a fragment F which uses the new Fragment Result API to get results from 2 other fragments: A, B gets the result from A but not from B because B has a different parent FragmentManager (and I don't know why) ? How could be something like that ? 2 fragments called in the same way but they end up having same Activity but different FragmentManager ? The function calls are the following:
//THIS DOESN'T WORK. THE LISTENER IS NOT CALLED AFTER THE RESULT IS SET
private fun navigateToItemLocation() {
setFragmentResultListener(REQUEST_LOCATION_KEY) { s: String, bundle: Bundle ->
val locationId = bundle.getParcelable<ParcelUuid>(LOCATION_ID)!!.uuid
viewModel.viewModelScope.launch(Dispatchers.IO) {
val location = LocationRepository().get(locationId)!!
changeItemLocation(location)
}
}
val action = ItemRegistrationPagerHolderDirections.actionNavItemRegistrationPagerToNavStockLocationSelection()
findNavController().navigate(action)
}
//THIS WORKS FINE:
private fun navigateToItemDetails(item: Item2) {
setFragmentResultListener(SELECTED_ITEM_KEY) { s: String, bundle: Bundle ->
val propertySetId = bundle.getParcelable<ParcelUuid>(SELECTED_ITEM_SET_ID)!!.uuid
clearFragmentResultListener(SELECTED_ITEM_KEY)
viewModel.viewModelScope.launch(Dispatchers.IO) {
val repository = PropertySetRepository()
val propertySet = repository.get(propertySetId)!!
val propertySetInfo = ItemFactory.loadPropertySetInfo(propertySet)
withContext(Dispatchers.Main) { setPackageCode(null) }
selectItem(item.item, propertySetInfo, item.description, null)
}
}
val action = ItemRegistrationPagerHolderDirections.actionNavItemRegistrationToNavStockItemDetails(ParcelUuid(item.item.id), true)
findNavController().navigate(action)
}
Both fragments A and B are in a separate Dynamic Feature. The only single problem I have is that when the following function is called:
fun onSelect() {
viewModel.pickedLocation.value = (viewModel.selectedLocation as? LocationExt2?)?.location
val result = bundleOf(Pair(LOCATION_ID, ParcelUuid(viewModel.pickedLocation.value!!.id)))
setFragmentResult(REQUEST_LOCATION_KEY, result)
findNavController().popBackStack()
}
setFragmentResult(REQUEST_LOCATION_KEY, result)
Doesn't produce any result because the FragmentManager is not the same of the calling Fragment. The same method in fragment A which is:
private fun onSetSelected(id: UUID) {
propertySets.removeObservers(viewLifecycleOwner)
adapter.tracker = null
setFragmentResult(SELECTED_ITEM_KEY, bundleOf(Pair(SELECTED_ITEM_SET_ID, ParcelUuid(id))))
findNavController().popBackStack()
}
As a temporarily workaround I replaced the call to Fragment's FragmentManager with Activity.supportFragmentManager.setFragmentResultListener. It works but still I do not understand why fragments A and B behave differently...
Check that fragment where you listen for fragment result and fragment where you set the result are in the same fragment manager.
Common case where this would happen is if you are using Activity.getSupportFragmentManager() or Fragment.getParentFragmentManager() alongside Fragment.getChildFragmentManager().
Check this blog article for the principles and rules with Fragment Result API on medium: https://medium.com/#FrederickKlyk/state-of-the-art-communication-between-fragments-and-their-activity-daa1fe4e014d
Only one listener can be registered for a specific request key.
If more than one listener is registered on the same key, the previous one will be replaced by the newest listener.
I am using fragment by supportFragmentManager
When I move to some Fragment and write down text in EditText, then go back(without any saving), and move again to that Fragment, EditText fill with I write down before.
I don't want to save the EditText state, but it save by itself....
Additionally, some Fragments don't save their viewState but other some Fragments save it.
The Fragment don't save their viewState are located first depth on Activity which means they are home screen at each tab.
On the other hand, The Fragment save their viewState by itself located second or more depth from first depth fragment(home screen)
This is FragmentNavigation code
class FragmentNavigation(private val activity: MainActivity) {
private val manager = activity.supportFragmentManager
private val fragmentMap = HashMap<Int, Stack<Fragment>>()
private var currentTab = 1 //This start destination fragment
private val container = R.id.nav_host_fragment
init { //I have 5 tabs
fragmentMap[0] = Stack<Fragment>()
fragmentMap[1] = Stack<Fragment>().apply { push(MemoFragment()) } //add starting point
fragmentMap[2] = Stack<Fragment>()
fragmentMap[3] = Stack<Fragment>()
fragmentMap[4] = Stack<Fragment>()
}
fun change(info: FragmentInfo) = //When user click bottomTab
fragmentMap[info.tag]?.let {
currentTab = info.tag
if (it.isEmpty()) {
it.push(info.fragment)
manager.commit {
replace(container, info.fragment)
}
return#let
}
//show last fragment I visited
manager.commit {
replace(container, it.last())
}
}
fun move(info: FragmentInfo) = //When user move to second or more depth fragment
fragmentMap[info.tag]?.let {
it.push(info.fragment)
manager.commit {
replace(container, info.fragment)
}
}
fun back() =
if(fragmentMap[currentTab]?.size == 1)
activity.finishApp()
else
popFragment()
private fun popFragment() =
fragmentMap[currentTab]?.let {
it.pop()
manager.commit {
replace(container, it.peek())
}
}
}
As above code say, first depth Fragments(don't save state) are shown by change function,
problematic fragment(save state) are shown by move function.
-> There is no onSaveInstanceState function in fragment.
-> Not using Bundle
-> I tried add and remove instead of replace as a fragmentTransaction, but same result..
-> tried remove all viewModel
-> Not using DataBinding
-> Tried Invalidate Cashes/Restart, Clean project, Rebuild Project many time.
-> remove and reinstall app so many time.